├── .gitignore ├── screenshot.png ├── content ├── seed.go ├── culture │ └── culture.go ├── gender │ └── gender.go ├── table │ └── table.go ├── format │ └── format.go ├── name │ ├── generator.go │ └── name.go ├── sector │ └── sector.go ├── alien.go ├── religion.go ├── corporation.go ├── heresy.go ├── bestiary.go ├── poi.go ├── encounter.go ├── conflict.go ├── place.go ├── beast.go ├── npc.go └── adventure.go ├── dice └── dice.go ├── go.mod ├── export ├── json.go ├── text.go ├── export.go └── hugo.go ├── LICENSE ├── main.go ├── cmd ├── react.go ├── culture.go ├── new.go ├── beast.go ├── poi.go ├── alien.go ├── heresy.go ├── tag.go ├── religion.go ├── conflict.go ├── corporation.go ├── show.go ├── adventure.go ├── place.go ├── encounter.go ├── bestiary.go ├── root.go ├── export.go ├── world.go ├── npc.go └── sector.go ├── haxscii └── haxscii.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | swnt 2 | build/ -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nboughton/swnt/HEAD/screenshot.png -------------------------------------------------------------------------------- /content/seed.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | rand.Seed(time.Now().UnixNano()) 10 | } 11 | -------------------------------------------------------------------------------- /dice/dice.go: -------------------------------------------------------------------------------- 1 | package dice 2 | 3 | import "github.com/nboughton/go-roll" 4 | 5 | // D5 used in a few roll tables 6 | var D5 = roll.NewDie(roll.Faces{{N: 1, Value: "1"}, {N: 2, Value: "2"}, {N: 3, Value: "3"}, {N: 4, Value: "4"}, {N: 5, Value: "5"}}) 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nboughton/swnt 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fatih/color v1.12.0 7 | github.com/mattn/go-isatty v0.0.13 // indirect 8 | github.com/nboughton/go-roll v0.0.17 9 | github.com/nboughton/go-utils v0.0.0-20200108161841-5007e997f484 10 | github.com/spf13/cobra v1.2.1 11 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /export/json.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nboughton/go-utils/json/file" 7 | "github.com/nboughton/swnt/content/sector" 8 | ) 9 | 10 | // JSON represents the Exporter for JSON data 11 | type JSON struct { 12 | Name string 13 | Stars *sector.Stars 14 | } 15 | 16 | func (j *JSON) Write() error { 17 | fmt.Println("Exporting as json...") 18 | 19 | return file.Write(j.Name+".json", j.Stars) 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2018 Nick Boughton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import "github.com/nboughton/swnt/cmd" 24 | 25 | func main() { 26 | cmd.Execute() 27 | } 28 | -------------------------------------------------------------------------------- /export/text.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "text/tabwriter" 9 | 10 | "github.com/nboughton/swnt/content/format" 11 | "github.com/nboughton/swnt/content/sector" 12 | ) 13 | 14 | // Text represents the Exporter for text based output 15 | type Text struct { 16 | Name string 17 | Stars *sector.Stars 18 | } 19 | 20 | func (t *Text) Write() error { 21 | fmt.Println("Exporting as plain text...") 22 | wdir, _ := os.Getwd() 23 | 24 | textDir := "text" 25 | if err := os.Mkdir(textDir, dirPerm); err != nil { 26 | return err 27 | } 28 | 29 | if err := os.Chdir(textDir); err != nil { 30 | return err 31 | } 32 | 33 | fmt.Println("Creating Stars dir...") 34 | starsDir := "Stars" 35 | if err := os.Mkdir(starsDir, dirPerm); err != nil { 36 | return err 37 | } 38 | 39 | for _, system := range t.Stars.Systems { 40 | buf := new(bytes.Buffer) 41 | tab := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0) 42 | 43 | fmt.Fprint(tab, system.Format(format.TEXT)) 44 | tab.Flush() 45 | 46 | ioutil.WriteFile(starsDir+"/"+system.Name+".txt", buf.Bytes(), filePerm) 47 | } 48 | 49 | mapDir := "Maps" 50 | if err := os.Mkdir(mapDir, dirPerm); err != nil { 51 | return err 52 | } 53 | 54 | ioutil.WriteFile(mapDir+"/gm-map.txt", []byte(Hexmap(t.Stars, false, false)), filePerm) 55 | ioutil.WriteFile(mapDir+"/pc-map.txt", []byte(Hexmap(t.Stars, false, true)), filePerm) 56 | ioutil.WriteFile(mapDir+"/gm-map-ansi.txt", []byte(Hexmap(t.Stars, true, false)), filePerm) 57 | ioutil.WriteFile(mapDir+"/pc-map-ansi.txt", []byte(Hexmap(t.Stars, true, true)), filePerm) 58 | 59 | return os.Chdir(wdir) 60 | } 61 | -------------------------------------------------------------------------------- /content/culture/culture.go: -------------------------------------------------------------------------------- 1 | // Package culture provides reference types for cultural bases found in SWN Free edition 2 | package culture 3 | 4 | import ( 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func init() { 12 | rand.Seed(time.Now().UnixNano()) 13 | } 14 | 15 | // Culture represents the name of supported culture 16 | type Culture string 17 | 18 | // Culture constants 19 | const ( 20 | Arabic Culture = "Arabic" 21 | Chinese Culture = "Chinese" 22 | English Culture = "English" 23 | Greek Culture = "Greek" 24 | Indian Culture = "Indian" 25 | Japanese Culture = "Japanese" 26 | Latin Culture = "Latin" 27 | Nigerian Culture = "Nigerian" 28 | Russian Culture = "Russian" 29 | Spanish Culture = "Spanish" 30 | Any Culture = "Any" 31 | ) 32 | 33 | // Cultures list 34 | var Cultures = []Culture{Arabic, Chinese, English, Greek, Indian, Japanese, Latin, Nigerian, Russian, Spanish} 35 | 36 | // Random returns a cultures' identifier and string name at random 37 | func Random() Culture { 38 | n := rand.Intn(len(Cultures)) 39 | return Cultures[n] 40 | } 41 | 42 | // Find returns the correct constant or an error if it does not exist 43 | func Find(name string) (Culture, error) { 44 | if strings.ToLower(name) == strings.ToLower(Any.String()) || name == "" { 45 | return Random(), nil 46 | } 47 | 48 | for _, c := range Cultures { 49 | if strings.ToLower(c.String()) == strings.ToLower(name) { 50 | return c, nil 51 | } 52 | } 53 | 54 | return Culture(""), fmt.Errorf("no culture found for \"%s\", options available are %s", name, Cultures) 55 | } 56 | 57 | func (c Culture) String() string { 58 | return string(c) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/react.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/nboughton/swnt/content" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // reactCmd represents the react command 31 | var reactCmd = &cobra.Command{ 32 | Use: "react", 33 | Short: "Make a reaction roll for an NPC", 34 | Long: ``, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | fmt.Println(content.Reaction.Roll()) 37 | }, 38 | } 39 | 40 | func init() { 41 | RootCmd.AddCommand(reactCmd) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/culture.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Jeremy Friesen 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/nboughton/swnt/content/culture" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // cultureCmd represents the culture command 31 | var cultureCmd = &cobra.Command{ 32 | Use: "culture", 33 | Short: "Generate a culture", 34 | Long: ``, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | 37 | fmt.Fprintf(tw, culture.Random().String()) 38 | tw.Flush() 39 | }, 40 | } 41 | 42 | func init() { 43 | newCmd.AddCommand(cultureCmd) 44 | } 45 | -------------------------------------------------------------------------------- /content/gender/gender.go: -------------------------------------------------------------------------------- 1 | // Package gender is a convenience package for handling gender identifiers. 2 | package gender 3 | 4 | /* Why only 3 options? 5 | 6 | Gender as a spectrum is an ever growing list of self identifying labels that no developer 7 | could ever hope to keep up with. That said the number of people that identify as non-binary 8 | is a very, very small subset of the population. I'm not trying to marginalise anyone that 9 | doesn't like the term "other" for non-binary genders. I just don't have the time or spoons 10 | to attempt to cater to them. 11 | */ 12 | 13 | import ( 14 | "fmt" 15 | "math/rand" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | func init() { 21 | rand.Seed(time.Now().UnixNano()) 22 | } 23 | 24 | // Gender is a shorthand type for IDing general labels 25 | type Gender string 26 | 27 | // Gender ids 28 | const ( 29 | Male Gender = "Male" 30 | Female Gender = "Female" 31 | Other Gender = "Other" 32 | Any Gender = "Any" 33 | ) 34 | 35 | // Genders supported 36 | var Genders = []Gender{Male, Female, Other} 37 | 38 | // Random returns a random Gender 39 | func Random() Gender { 40 | n := rand.Intn(len(Genders)) 41 | return Genders[n] 42 | } 43 | 44 | // Find returns the id constant or an error if it doesn't exist 45 | func Find(name string) (Gender, error) { 46 | if strings.ToLower(name) == strings.ToLower(Any.String()) || name == "" { 47 | return Random(), nil 48 | } 49 | 50 | for _, g := range Genders { 51 | if strings.ToLower(g.String()) == strings.ToLower(name) { 52 | return g, nil 53 | } 54 | } 55 | 56 | return Gender(""), fmt.Errorf("no gender found for \"%s\", options available are %s", name, Genders) 57 | } 58 | 59 | func (g Gender) String() string { 60 | return string(g) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/new.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "os" 25 | "text/tabwriter" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // Tabwriter for formatting 31 | var tw = tabwriter.NewWriter(os.Stdout, 1, 2, 1, ' ', 0) 32 | 33 | // newCmd represents the create command 34 | var newCmd = &cobra.Command{ 35 | Use: "new", 36 | Short: "Generate content", 37 | Long: ``, 38 | } 39 | 40 | func init() { 41 | RootCmd.AddCommand(newCmd) 42 | newCmd.PersistentFlags().StringP(flFormat, "f", "txt", "Set output format. (--format txt,md). Not all commands support this flag.") 43 | } 44 | -------------------------------------------------------------------------------- /cmd/beast.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // beastCmd represents the beast command 33 | var beastCmd = &cobra.Command{ 34 | Use: "beast", 35 | Short: "Generate a Beast", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | b := content.NewBeast() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, b.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(beastCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/poi.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // poiCmd represents the poi command 33 | var poiCmd = &cobra.Command{ 34 | Use: "poi", 35 | Short: "Generate a Point of Interest", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | p := content.NewPOI() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, p.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(poiCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/alien.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Jeremy Friesen 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // cultureCmd represents the culture command 33 | var alienCmd = &cobra.Command{ 34 | Use: "alien", 35 | Short: "Generate an Alien", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | a := content.NewAlien() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, a.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(alienCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/heresy.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Jeremy Friesen 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // heresyCmd represents the heresy command 33 | var heresyCmd = &cobra.Command{ 34 | Use: "heresy", 35 | Short: "Generate a Heresy", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | h := content.NewHeresy() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, h.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(heresyCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/tag.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | // tagCmd represents the tag command 32 | var tagCmd = &cobra.Command{ 33 | Use: "tag", 34 | Short: "Print the text of a world tag", 35 | Long: ``, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | descOnly, _ := cmd.Flags().GetBool(flDescOnly) 38 | 39 | tag := strings.Join(args, " ") 40 | t, err := content.Tags.Find(tag) 41 | if err != nil { 42 | fmt.Println(err) 43 | return 44 | } 45 | 46 | if descOnly { 47 | fmt.Println(t.Desc) 48 | } else { 49 | fmt.Fprint(tw, t) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | showCmd.AddCommand(tagCmd) 57 | tagCmd.Flags().BoolP(flDescOnly, "d", false, "Print description only") 58 | } 59 | -------------------------------------------------------------------------------- /cmd/religion.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Jeremy Friesen 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // religionCmd represents the religion command 33 | var religionCmd = &cobra.Command{ 34 | Use: "religion", 35 | Short: "Generate a Religion", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | r := content.NewReligion() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, r.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(religionCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/conflict.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // conflictCmd represents the conflict command 33 | var conflictCmd = &cobra.Command{ 34 | Use: "conflict", 35 | Short: "Generate a Conflict/Problem", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | c := content.NewConflict() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, c.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(conflictCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/corporation.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Jeremy Friesen 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // corporationCmd represents the corporation command 33 | var corporationCmd = &cobra.Command{ 34 | Use: "corporation", 35 | Short: "Generate a Corporation", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | 40 | c := content.NewCorporation() 41 | for _, f := range strings.Split(fmc, ",") { 42 | fID, err := format.Find(f) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | 48 | fmt.Fprintf(tw, c.Format(fID)) 49 | fmt.Fprintln(tw) 50 | tw.Flush() 51 | } 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(corporationCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/show.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | // showCmd represents the show command 28 | var showCmd = &cobra.Command{ 29 | Use: "show", 30 | Short: "Print the text of a world tag, table text will be added in future", 31 | Long: ``, 32 | //Run: func(cmd *cobra.Command, args []string) { 33 | // fmt.Println("show called") 34 | //}, 35 | } 36 | 37 | func init() { 38 | RootCmd.AddCommand(showCmd) 39 | 40 | // Here you will define your flags and configuration settings. 41 | 42 | // Cobra supports Persistent Flags which will work for this command 43 | // and all subcommands, e.g.: 44 | // showCmd.PersistentFlags().String("foo", "", "A help for foo") 45 | 46 | // Cobra supports local flags which will only run when this command 47 | // is called directly, e.g.: 48 | // showCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 49 | } 50 | -------------------------------------------------------------------------------- /export/export.go: -------------------------------------------------------------------------------- 1 | // Package export provides Exporter types for writing project directories 2 | package export 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/nboughton/swnt/content/sector" 10 | "github.com/nboughton/swnt/haxscii" 11 | ) 12 | 13 | var ( 14 | dirPerm = os.FileMode(0755) 15 | filePerm = os.FileMode(0644) 16 | ) 17 | 18 | // Exporter represents any type that can Setup an export directory and output data to it. 19 | type Exporter interface { 20 | Write() error 21 | } 22 | 23 | // New returns a new Exporter. Export types currently supported are: hugo, txt and json 24 | func New(exportType, name string, data *sector.Stars) (Exporter, error) { 25 | switch exportType { 26 | case "hugo": 27 | return &Hugo{ 28 | Name: name, 29 | Stars: data, 30 | }, nil 31 | 32 | case "txt": 33 | return &Text{ 34 | Name: name, 35 | Stars: data, 36 | }, nil 37 | case "json": 38 | return &JSON{ 39 | Name: name, 40 | Stars: data, 41 | }, nil 42 | } 43 | 44 | return nil, fmt.Errorf("no Exporter found for [%s], available options are [%s]", exportType, []string{"hugo", "txt", "json"}) 45 | } 46 | 47 | // Hexmap returns the ASCII representation of a Sector map 48 | func Hexmap(data *sector.Stars, useColour bool, playerMap bool) string { 49 | haxscii.Colour(useColour) 50 | h := haxscii.NewMap(data.Rows, data.Cols) 51 | for _, s := range data.Systems { 52 | name, tag1, tag2, tl := s.Name, s.Worlds[0].Tags[0].Name, s.Worlds[0].Tags[1].Name, strings.Split(s.Worlds[0].TechLevel, ",")[0] 53 | c := haxscii.White // I default to black/dark terminals, this might be problematic for weirdos that use light terms 54 | 55 | switch tl { 56 | case "TL0": 57 | c = haxscii.White 58 | case "TL1": 59 | c = haxscii.Red 60 | case "TL2": 61 | c = haxscii.Yellow 62 | case "TL3": 63 | c = haxscii.Magenta 64 | case "TL4", "TL4+": 65 | c = haxscii.Green 66 | case "TL5": 67 | c = haxscii.Cyan 68 | } 69 | 70 | if playerMap { 71 | h.SetTxt(s.Row, s.Col, [4]string{name, "", "", ""}, c) 72 | } else { 73 | h.SetTxt(s.Row, s.Col, [4]string{name, tag1, tag2, tl}, c) 74 | } 75 | } 76 | 77 | return h.String() 78 | } 79 | -------------------------------------------------------------------------------- /cmd/adventure.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/nboughton/swnt/content" 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | // adventureCmd represents the adventure command 31 | var adventureCmd = &cobra.Command{ 32 | Use: "adventure", 33 | Short: "Generate an Adventure seed", 34 | Long: ``, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | tag, _ := cmd.Flags().GetString(flTag) 37 | list, _ := cmd.Flags().GetBool(flTags) 38 | 39 | if list { 40 | for _, t := range content.Tags { 41 | fmt.Println(t.Name) 42 | } 43 | return 44 | } 45 | 46 | if tag == "" { 47 | tag = content.Tags.Random() 48 | } 49 | 50 | fmt.Fprintln(tw, content.NewAdventure(tag).String()) 51 | tw.Flush() 52 | }, 53 | } 54 | 55 | func init() { 56 | newCmd.AddCommand(adventureCmd) 57 | adventureCmd.Flags().StringP(flTag, "t", "", "Specify world tag to base roll on.") 58 | adventureCmd.Flags().BoolP(flTags, "l", false, "List available tags.") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/place.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // placeCmd represents the place command 33 | var placeCmd = &cobra.Command{ 34 | Use: "place", 35 | Short: "Generate a place", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | w, _ := cmd.Flags().GetBool(flWilderness) 39 | fmc, _ := cmd.Flags().GetString(flFormat) 40 | 41 | p := content.NewPlace(w) 42 | for _, f := range strings.Split(fmc, ",") { 43 | fID, err := format.Find(f) 44 | if err != nil { 45 | fmt.Println(err) 46 | return 47 | } 48 | 49 | fmt.Fprintf(tw, p.Format(fID)) 50 | fmt.Fprintln(tw) 51 | tw.Flush() 52 | } 53 | }, 54 | } 55 | 56 | func init() { 57 | newCmd.AddCommand(placeCmd) 58 | placeCmd.Flags().BoolP(flWilderness, "w", false, "Set location to Wilderness (default is urban/civilisation)") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/encounter.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // encounterCmd represents the encounter command 33 | var encounterCmd = &cobra.Command{ 34 | Use: "encounter", 35 | Short: "Generate a quick encounter", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | wild, _ := cmd.Flags().GetBool(flWilderness) 39 | fmc, _ := cmd.Flags().GetString(flFormat) 40 | 41 | e := content.NewEncounter(wild) 42 | for _, f := range strings.Split(fmc, ",") { 43 | fID, err := format.Find(f) 44 | if err != nil { 45 | fmt.Println(err) 46 | return 47 | } 48 | 49 | fmt.Fprintf(tw, e.Format(fID)) 50 | fmt.Fprintln(tw) 51 | tw.Flush() 52 | } 53 | }, 54 | } 55 | 56 | func init() { 57 | newCmd.AddCommand(encounterCmd) 58 | encounterCmd.Flags().BoolP(flWilderness, "w", false, "Set encounter type to Wilderness (default is Urban)") 59 | } 60 | -------------------------------------------------------------------------------- /content/table/table.go: -------------------------------------------------------------------------------- 1 | // Package table expands on github.com/nboughton/roll to provide some custom table types that are 2 | // routinely used in SWN Free Edition 3 | package table 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/nboughton/go-roll" 11 | ) 12 | 13 | // Registry of tables 14 | var Registry = roll.NewTableRegistry() 15 | 16 | func init() { 17 | rand.Seed(time.Now().UnixNano()) 18 | } 19 | 20 | // ThreePart represents a relatively common structure for multi-layer tables in 21 | // SWN (RE) 22 | type ThreePart struct { 23 | Headers [3]string 24 | Tables []ThreePartSubTable 25 | } 26 | 27 | // Roll performs all rolls on a ThreePart table 28 | func (t ThreePart) Roll() [][]string { 29 | i := rand.Intn(len(t.Tables)) 30 | 31 | return [][]string{ 32 | {t.Headers[0], t.Tables[i].Name}, 33 | {t.Headers[1], t.Tables[i].SubTable1.Roll()}, 34 | {t.Headers[2], t.Tables[i].SubTable2.Roll()}, 35 | } 36 | } 37 | 38 | func (t ThreePart) String() string { 39 | s := "" 40 | for _, sub := range t.Tables { 41 | s += sub.String() 42 | } 43 | 44 | return s 45 | } 46 | 47 | // ThreePartSubTable represent the subtables of a ThreePart 48 | type ThreePartSubTable struct { 49 | Name string 50 | SubTable1 roll.Tabler 51 | SubTable2 roll.Tabler 52 | } 53 | 54 | // String satisfies the Stringer interface for threePartSubTables 55 | func (t ThreePartSubTable) String() string { 56 | return fmt.Sprintf("%s\n%s\n%s", t.Name, t.SubTable1, t.SubTable2) 57 | } 58 | 59 | // OneRoll represents the oft used one-roll systems spread throughout SWN 60 | type OneRoll struct { 61 | D4 roll.Tabler 62 | D6 roll.Tabler 63 | D8 roll.Tabler 64 | D10 roll.Tabler 65 | D12 roll.Tabler 66 | D20 roll.Tabler 67 | } 68 | 69 | // Roll performs all rolls for a OneRoll and returns the results 70 | func (o OneRoll) Roll() [][]string { 71 | return [][]string{ 72 | {o.D4.Label(), o.D4.Roll()}, 73 | {o.D6.Label(), o.D6.Roll()}, 74 | {o.D8.Label(), o.D8.Roll()}, 75 | {o.D10.Label(), o.D10.Roll()}, 76 | {o.D12.Label(), o.D12.Roll()}, 77 | {o.D20.Label(), o.D20.Roll()}, 78 | } 79 | } 80 | 81 | func (o OneRoll) String() string { 82 | return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n", o.D4, o.D6, o.D8, o.D10, o.D12, o.D20) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/bestiary.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/format" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // bestiaryCmd represents the bestiary command 33 | var bestiaryCmd = &cobra.Command{ 34 | Use: "bestiary", 35 | Short: "List creature statblocks", 36 | Long: ``, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | fmc, _ := cmd.Flags().GetString(flFormat) 39 | flt, _ := cmd.Flags().GetStringArray(flFilter) 40 | 41 | s := content.StatBlocks.Filter(flt...) 42 | for _, f := range strings.Split(fmc, ",") { 43 | fID, err := format.Find(f) 44 | if err != nil { 45 | fmt.Println(err) 46 | return 47 | } 48 | 49 | fmt.Fprintf(tw, s.Format(fID)) 50 | fmt.Fprintln(tw) 51 | } 52 | 53 | tw.Flush() 54 | }, 55 | } 56 | 57 | func init() { 58 | RootCmd.AddCommand(bestiaryCmd) 59 | 60 | bestiaryCmd.Flags().StringArrayP(flFilter, "l", []string{}, "Filter by name. I.e -l \"human\" -l \"pirate\"") 61 | bestiaryCmd.Flags().StringP(flFormat, "f", "txt", "Set output format. (--format txt,md). Not all commands support this flag.") 62 | } 63 | -------------------------------------------------------------------------------- /content/format/format.go: -------------------------------------------------------------------------------- 1 | // Package format provides formatting functions for text output 2 | package format 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // OutputType defines identifiers for output style categories such as text and markdown 11 | type OutputType string 12 | 13 | // OutputType constants 14 | const ( 15 | TEXT OutputType = "txt" 16 | MARKDOWN OutputType = "md" 17 | ) 18 | 19 | // Types of format output currently supported 20 | var Types = []OutputType{TEXT, MARKDOWN} 21 | 22 | func (o OutputType) String() string { 23 | return string(o) 24 | } 25 | 26 | // Find attempts retrieve the OutputType value from string 27 | func Find(name string) (OutputType, error) { 28 | for _, t := range Types { 29 | if strings.ToLower(name) == t.String() { 30 | return t, nil 31 | } 32 | } 33 | 34 | return "", fmt.Errorf("format not supported, available output types are: %s", Types) 35 | } 36 | 37 | // Header formats and returns a header in format t, header sizes are defined in HTML/MARKDOWN terms with 1 being the largest 38 | // and reducing in size as the number increases. 39 | func Header(t OutputType, size int, text string) string { 40 | out := "" 41 | 42 | switch t { 43 | case TEXT: 44 | out = text + "\n" 45 | 46 | case MARKDOWN: 47 | out = fmt.Sprintf("%s %s\n\n", strings.Repeat("#", size), text) 48 | } 49 | 50 | return out 51 | } 52 | 53 | // Table returns a formatted Table of type t. Headers are optional so that different bits of content 54 | // can be concatenated into a single table through multiple calls to Table. Bear in mind that Markdown 55 | // tables must start with a header though. 56 | func Table(t OutputType, headers []string, rows [][]string) string { 57 | buf, sep, rowTmpl := new(bytes.Buffer), "", "" 58 | 59 | if len(rows) == 0 { 60 | return "no table data found" 61 | } 62 | 63 | switch t { 64 | case TEXT: 65 | sep, rowTmpl = "\t:\t", "%s\n" 66 | 67 | if len(headers) > 0 { 68 | fmt.Fprintf(buf, "%s\n", strings.Join(headers, sep)) 69 | } 70 | case MARKDOWN: 71 | sep, rowTmpl = " | ", "| %s |\n" 72 | 73 | if len(headers) > 0 { 74 | cells := make([]string, len(rows[0])) 75 | fmt.Fprintf(buf, "| %s |\n| %s --- |\n", strings.Join(headers, " | "), strings.Join(cells, " --- |")) 76 | } 77 | } 78 | 79 | for _, row := range rows { 80 | fmt.Fprintf(buf, rowTmpl, strings.Join(row, sep)) 81 | } 82 | 83 | return buf.String() 84 | } 85 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | const ( 31 | flExclude = "exclude" 32 | flFormat = "format" 33 | flColour = "colour" 34 | flPoi = "poi-chance" 35 | flOW = "other-worlds-chance" 36 | flSecHeight = "sector-height" 37 | flSecWidth = "sector-width" 38 | flExport = "export" 39 | flDensity = "density" 40 | 41 | flFile = "file" 42 | 43 | flWilderness = "wilderness" 44 | 45 | flCulture = "culture" 46 | flGender = "gender" 47 | flPatron = "is-patron" 48 | 49 | flDescOnly = "desc-only" 50 | flTag = "tag" 51 | flTags = "tags" 52 | flLongTags = "long-tags" 53 | 54 | flList = "list" 55 | flName = "name" 56 | flFilter = "filter" 57 | flAll = "all" 58 | ) 59 | 60 | // RootCmd represents the base command when called without any subcommands 61 | var RootCmd = &cobra.Command{ 62 | Use: "swnt", 63 | Short: "A simple application for generating content for Stars Without Number", 64 | Long: ``, 65 | } 66 | 67 | // Execute adds all child commands to the root command and sets flags appropriately. 68 | // This is called by main.main(). It only needs to happen once to the rootCmd. 69 | func Execute() { 70 | if err := RootCmd.Execute(); err != nil { 71 | fmt.Println(err) 72 | os.Exit(1) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cmd/export.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "log" 26 | "strings" 27 | 28 | "github.com/nboughton/go-utils/json/file" 29 | "github.com/nboughton/swnt/content/sector" 30 | "github.com/nboughton/swnt/export" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | // exportCmd represents the export command 35 | var exportCmd = &cobra.Command{ 36 | Use: "export", 37 | Short: "Export a json dump to hugo or text", 38 | Long: ``, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | jsonFile, _ := cmd.Flags().GetString(flFile) 41 | exportTypes, _ := cmd.Flags().GetString(flExport) 42 | 43 | secName := strings.Replace(jsonFile, ".json", "", -1) 44 | 45 | secData := new(sector.Stars) 46 | if err := file.Scan(jsonFile, &secData); err != nil { 47 | fmt.Println("Error reading file. You may need to reformat the JSON data to make it more easily readable.", err) 48 | return 49 | } 50 | 51 | for _, t := range strings.Split(exportTypes, ",") { 52 | if exporter, err := export.New(t, secName, secData); exporter != nil { 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | if err = exporter.Write(); err != nil { 58 | log.Fatal(err) 59 | } 60 | } 61 | } 62 | }, 63 | } 64 | 65 | func init() { 66 | RootCmd.AddCommand(exportCmd) 67 | exportCmd.Flags().StringP(flFile, "i", "", "Path to json file") 68 | exportCmd.Flags().StringP(flExport, "x", "hugo,txt", "Set export format") 69 | } 70 | -------------------------------------------------------------------------------- /cmd/world.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "strings" 26 | 27 | "github.com/nboughton/swnt/content" 28 | "github.com/nboughton/swnt/content/culture" 29 | "github.com/nboughton/swnt/content/format" 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | // worldCmd represents the world command 34 | var worldCmd = &cobra.Command{ 35 | Use: "world", 36 | Short: "Generate a secondary World for a Sector cell", 37 | Long: ``, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | var ( 40 | ctr, _ = cmd.Flags().GetString(flCulture) 41 | exc, _ = cmd.Flags().GetStringArray(flExclude) 42 | flt, _ = cmd.Flags().GetBool(flLongTags) 43 | fmc, _ = cmd.Flags().GetString(flFormat) 44 | cID culture.Culture 45 | err error 46 | ) 47 | 48 | if ctr == "" { 49 | cID = culture.Random() 50 | } else { 51 | cID, err = culture.Find(ctr) 52 | if err != nil { 53 | fmt.Printf("No Culture found for \"%s\", options are %v\n", ctr, culture.Cultures) 54 | return 55 | } 56 | } 57 | 58 | w := content.NewWorld(false, cID, flt, exc) 59 | for _, f := range strings.Split(fmc, ",") { 60 | fID, err := format.Find(f) 61 | if err != nil { 62 | fmt.Println(err) 63 | return 64 | } 65 | 66 | fmt.Fprintf(tw, w.Format(fID)) 67 | fmt.Fprintln(tw) 68 | tw.Flush() 69 | } 70 | }, 71 | } 72 | 73 | func init() { 74 | newCmd.AddCommand(worldCmd) 75 | worldCmd.Flags().StringP(flCulture, "c", "", "Set Culture of world") 76 | worldCmd.Flags().BoolP(flLongTags, "l", false, "Toggle full world tag info in output") 77 | worldCmd.Flags().StringArrayP(flExclude, "x", []string{}, "Exclude tags (-x zombies -x \"regional hegemon\" etc)") 78 | } 79 | -------------------------------------------------------------------------------- /cmd/npc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "math/rand" 26 | "strings" 27 | "time" 28 | 29 | "github.com/nboughton/swnt/content" 30 | "github.com/nboughton/swnt/content/culture" 31 | "github.com/nboughton/swnt/content/format" 32 | "github.com/nboughton/swnt/content/gender" 33 | "github.com/spf13/cobra" 34 | ) 35 | 36 | // npcCmd represents the person command 37 | var npcCmd = &cobra.Command{ 38 | Use: "npc", 39 | Short: "Generate a NPC", 40 | Long: ``, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | var ( 43 | clt, _ = cmd.Flags().GetString(flCulture) 44 | gdr, _ = cmd.Flags().GetString(flGender) 45 | isPatron, _ = cmd.Flags().GetBool(flPatron) 46 | fmc, _ = cmd.Flags().GetString(flFormat) 47 | err error 48 | ) 49 | 50 | cID, err := culture.Find(clt) 51 | if err != nil { 52 | fmt.Println(err) 53 | return 54 | } 55 | 56 | gID, err := gender.Find(gdr) 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | 62 | n := content.NewNPC(cID, gID, isPatron) 63 | for _, f := range strings.Split(fmc, ",") { 64 | fID, err := format.Find(f) 65 | if err != nil { 66 | fmt.Println(err) 67 | return 68 | } 69 | 70 | fmt.Fprintf(tw, n.Format(fID)) 71 | fmt.Fprintln(tw) 72 | tw.Flush() 73 | } 74 | }, 75 | } 76 | 77 | func init() { 78 | rand.Seed(time.Now().UnixNano()) 79 | 80 | newCmd.AddCommand(npcCmd) 81 | npcCmd.Flags().StringP(flCulture, "c", "any", fmt.Sprintf("Select Culture, choices are: %v", culture.Cultures)) 82 | npcCmd.Flags().StringP(flGender, "g", "", fmt.Sprintf("Select Gender, choices are: %v", gender.Genders)) 83 | npcCmd.Flags().BoolP(flPatron, "p", false, "NPC is a Patron") 84 | } 85 | -------------------------------------------------------------------------------- /export/hugo.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/nboughton/swnt/content/format" 9 | "github.com/nboughton/swnt/content/sector" 10 | ) 11 | 12 | // Hugo represents the Exporter for Hugo projects 13 | type Hugo struct { 14 | Name string 15 | Stars *sector.Stars 16 | } 17 | 18 | // Write satisfies the Setup requirement of the Exporter interface 19 | func (h *Hugo) Write() error { 20 | fmt.Println("Exporting as hugo site...") 21 | wdir, _ := os.Getwd() 22 | 23 | hugoDir := "hugo" 24 | if err := os.Mkdir(hugoDir, dirPerm); err != nil { 25 | return err 26 | } 27 | 28 | if err := os.Chdir(hugoDir); err != nil { 29 | return err 30 | } 31 | 32 | fmt.Println("Creating new hugo site...") 33 | o, err := exec.Command("hugo", "new", "site", ".").CombinedOutput() 34 | if err != nil { 35 | return err 36 | } 37 | fmt.Print(string(o)) 38 | 39 | o, err = exec.Command("git", "init").CombinedOutput() 40 | if err != nil { 41 | return err 42 | } 43 | fmt.Print(string(o)) 44 | 45 | o, err = exec.Command("git", "submodule", "add", "https://github.com/nboughton/hugo-theme-docdock.git", "themes/docdock").CombinedOutput() 46 | if err != nil { 47 | return err 48 | } 49 | fmt.Print(string(o)) 50 | 51 | o, err = exec.Command("git", "submodule", "init").CombinedOutput() 52 | if err != nil { 53 | return err 54 | } 55 | fmt.Print(string(o)) 56 | 57 | o, err = exec.Command("git", "submodule", "update").CombinedOutput() 58 | if err != nil { 59 | return err 60 | } 61 | fmt.Print(string(o)) 62 | 63 | fmt.Println("Copying config...") 64 | _, err = exec.Command("cp", "themes/docdock/exampleSite/config.toml", ".").CombinedOutput() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | fmt.Println("Setting Title...") 70 | _, err = exec.Command("sed", "-i", fmt.Sprintf("s/TITLE/%s/", h.Name), "config.toml").CombinedOutput() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | fmt.Println("Copying in default archetype...") 76 | _, err = exec.Command("cp", "themes/docdock/archetypes/default.md", "archetypes/").CombinedOutput() 77 | if err != nil { 78 | return err 79 | } 80 | 81 | fmt.Println("Creating Stars dir...") 82 | starsDir := "content/Stars" 83 | if err := os.Mkdir(starsDir, dirPerm); err != nil { 84 | return err 85 | } 86 | 87 | fmt.Println("Populating Stars dir...") 88 | for _, star := range h.Stars.Systems { 89 | // Create article stub 90 | o, err := exec.Command("hugo", "new", fmt.Sprintf("Stars/%s.md", star.Name)).CombinedOutput() 91 | if err != nil { 92 | return err 93 | } 94 | fmt.Print(string(o)) 95 | 96 | // Append Star data using Markdown 97 | f, err := os.OpenFile(fmt.Sprintf("%s/%s.md", starsDir, star.Name), os.O_APPEND|os.O_WRONLY, filePerm) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if _, err := f.Write([]byte(star.Format(format.MARKDOWN))); err != nil { 103 | return err 104 | } 105 | 106 | f.Close() 107 | } 108 | 109 | // Print hexmap to index.md 110 | o, err = exec.Command("hugo", "new", "_index.md").CombinedOutput() 111 | if err != nil { 112 | return err 113 | } 114 | fmt.Println(string(o)) 115 | 116 | f, err := os.OpenFile("content/_index.md", os.O_APPEND|os.O_WRONLY, filePerm) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | if _, err := f.Write([]byte("# " + h.Name + "\n\n```\n" + Hexmap(h.Stars, false, false) + "\n```")); err != nil { 122 | return err 123 | } 124 | 125 | return os.Chdir(wdir) 126 | } 127 | -------------------------------------------------------------------------------- /content/name/generator.go: -------------------------------------------------------------------------------- 1 | package name 2 | 3 | import ( 4 | "math/rand" 5 | "regexp" 6 | "strings" 7 | "time" 8 | 9 | "github.com/nboughton/go-roll" 10 | "github.com/nboughton/swnt/dice" 11 | ) 12 | 13 | var badPrefix = regexp.MustCompile(`[cflmnr][^aeiouyh]`) 14 | 15 | func init() { 16 | rand.Seed(time.Now().UnixNano()) 17 | } 18 | 19 | // Name generator. This is a primitive first effort that will be refined over time 20 | 21 | // Generate creates a random name by combining alternating vowels and consonants 22 | func Generate(ln int) string { 23 | name := "" 24 | for i := rand.Intn(2); len(name) <= ln; i++ { 25 | if i%2 != 0 { 26 | c := con.Roll() 27 | 28 | if name == "" { 29 | for badPrefix.MatchString(c) { // I don't like starting a name with these 30 | c = con.Roll() 31 | } 32 | } 33 | 34 | name += c 35 | } else { 36 | name += vl.Roll() 37 | } 38 | } 39 | 40 | return strings.ToUpper(string(name[0])) + string(name[1:]) 41 | } 42 | 43 | var vl = roll.Table{ 44 | Name: "vowels", 45 | Dice: roll.Dice{N: 10, Die: dice.D5}, 46 | Items: []roll.TableItem{ 47 | {Match: []int{10}, Text: "ii"}, 48 | {Match: []int{11}, Text: "yu"}, 49 | {Match: []int{12}, Text: "uy"}, 50 | {Match: []int{13}, Text: "oy"}, 51 | {Match: []int{14}, Text: "ao"}, 52 | {Match: []int{15}, Text: "ye"}, 53 | {Match: []int{16}, Text: "ae"}, 54 | {Match: []int{17}, Text: "oe"}, 55 | {Match: []int{18}, Text: "eo"}, 56 | {Match: []int{19}, Text: "oi"}, 57 | {Match: []int{20}, Text: "ua"}, 58 | {Match: []int{21}, Text: "au"}, 59 | {Match: []int{22}, Text: "ia"}, 60 | {Match: []int{23}, Text: "ey"}, 61 | {Match: []int{24}, Text: "oo"}, 62 | {Match: []int{25}, Text: "io"}, 63 | {Match: []int{26}, Text: "ea"}, 64 | {Match: []int{27}, Text: "y"}, 65 | {Match: []int{28}, Text: "o"}, 66 | {Match: []int{29}, Text: "a"}, 67 | {Match: []int{30}, Text: "e"}, 68 | {Match: []int{31}, Text: "i"}, 69 | {Match: []int{32}, Text: "u"}, 70 | {Match: []int{33}, Text: "ou"}, 71 | {Match: []int{34}, Text: "ee"}, 72 | {Match: []int{35}, Text: "ai"}, 73 | {Match: []int{36}, Text: "ie"}, 74 | {Match: []int{37}, Text: "ei"}, 75 | {Match: []int{38}, Text: "ue"}, 76 | {Match: []int{39}, Text: "ay"}, 77 | {Match: []int{40}, Text: "ui"}, 78 | {Match: []int{41}, Text: "oa"}, 79 | {Match: []int{42}, Text: "yi"}, 80 | {Match: []int{43}, Text: "ya"}, 81 | {Match: []int{44}, Text: "eu"}, 82 | {Match: []int{45}, Text: "iu"}, 83 | {Match: []int{46}, Text: "yo"}, 84 | {Match: []int{47}, Text: "aa"}, 85 | {Match: []int{48}, Text: "uo"}, 86 | {Match: []int{49}, Text: "uu"}, 87 | {Match: []int{50}, Text: "'"}, 88 | }, 89 | } 90 | 91 | var con = roll.Table{ 92 | Name: "consonants", 93 | Dice: roll.Dice{N: 10, Die: dice.D5}, 94 | Items: []roll.TableItem{ 95 | {Match: []int{10}, Text: "tt"}, 96 | {Match: []int{11}, Text: "rr"}, 97 | {Match: []int{12}, Text: "ct"}, 98 | {Match: []int{13}, Text: "pr"}, 99 | {Match: []int{14}, Text: "ns"}, 100 | {Match: []int{15}, Text: "bl"}, 101 | {Match: []int{16}, Text: "sh"}, 102 | {Match: []int{17}, Text: "ld"}, 103 | {Match: []int{18}, Text: "k"}, 104 | {Match: []int{19}, Text: "nd"}, 105 | {Match: []int{20}, Text: "ll"}, 106 | {Match: []int{21}, Text: "nt"}, 107 | {Match: []int{22}, Text: "st"}, 108 | {Match: []int{23}, Text: "f"}, 109 | {Match: []int{24}, Text: "ng"}, 110 | {Match: []int{25}, Text: "w"}, 111 | {Match: []int{26}, Text: "th"}, 112 | {Match: []int{27}, Text: "m"}, 113 | {Match: []int{28}, Text: "n"}, 114 | {Match: []int{29}, Text: "d"}, 115 | {Match: []int{30}, Text: "r"}, 116 | {Match: []int{31}, Text: "s"}, 117 | {Match: []int{32}, Text: "t"}, 118 | {Match: []int{33}, Text: "l"}, 119 | {Match: []int{34}, Text: "c"}, 120 | {Match: []int{35}, Text: "v"}, 121 | {Match: []int{36}, Text: "b"}, 122 | {Match: []int{37}, Text: "p"}, 123 | {Match: []int{38}, Text: "h"}, 124 | {Match: []int{39}, Text: "g"}, 125 | {Match: []int{40}, Text: "wh"}, 126 | {Match: []int{41}, Text: "ch"}, 127 | {Match: []int{42}, Text: "ss"}, 128 | {Match: []int{43}, Text: "rs"}, 129 | {Match: []int{44}, Text: "nc"}, 130 | {Match: []int{45}, Text: "fr"}, 131 | {Match: []int{46}, Text: "rt"}, 132 | {Match: []int{47}, Text: "gr"}, 133 | {Match: []int{48}, Text: "rd"}, 134 | {Match: []int{49}, Text: "sp"}, 135 | {Match: []int{50}, Text: "ck"}, 136 | }, 137 | } 138 | -------------------------------------------------------------------------------- /content/sector/sector.go: -------------------------------------------------------------------------------- 1 | // Package sector builds on other table packages to generate a complete sector 2 | package sector 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/nboughton/swnt/content" 11 | "github.com/nboughton/swnt/content/culture" 12 | "github.com/nboughton/swnt/content/format" 13 | "github.com/nboughton/swnt/content/name" 14 | ) 15 | 16 | func init() { 17 | rand.Seed(time.Now().UnixNano()) 18 | } 19 | 20 | // Star represents a single Star on the Sector map 21 | type Star struct { 22 | Row, Col int 23 | Culture culture.Culture 24 | Name string 25 | Worlds []content.World 26 | POIs []content.POI 27 | } 28 | 29 | // NewStar generates a new Star struct to be added to the map 30 | func NewStar(row, col int, name string, exclude []string, fullTags bool, poiChance, otherWorldChance int) *Star { 31 | ctr := culture.Random() 32 | 33 | s := &Star{ 34 | Row: row, 35 | Col: col, 36 | Culture: ctr, 37 | Name: name, 38 | Worlds: []content.World{content.NewWorld(true, ctr, fullTags, exclude)}, 39 | } 40 | 41 | // Cascading 10% chance of other worlds 42 | for rand.Intn(100) < otherWorldChance { 43 | ctr = culture.Random() 44 | s.Worlds = append(s.Worlds, content.NewWorld(false, ctr, fullTags, exclude)) 45 | } 46 | 47 | // 30% chance of a Point of Interest 48 | if rand.Intn(100) < poiChance { 49 | s.POIs = append(s.POIs, content.NewPOI()) 50 | } 51 | 52 | return s 53 | } 54 | 55 | // Format returns the details of a Star formatted as type t 56 | func (s *Star) Format(t format.OutputType) string { 57 | buf := new(bytes.Buffer) 58 | 59 | fmt.Fprintf(buf, format.Header(t, 2, fmt.Sprintf("Hex: %d,%d", s.Row, s.Col))) 60 | fmt.Fprintf(buf, format.Header(t, 3, "Primary World")) 61 | fmt.Fprintf(buf, s.Worlds[0].Format(t)) 62 | 63 | if len(s.Worlds) > 1 { 64 | fmt.Fprintf(buf, format.Header(t, 3, "Other Worlds")) 65 | for _, w := range s.Worlds[1:] { 66 | fmt.Fprintln(buf, w.Format(t)) 67 | } 68 | } 69 | 70 | if len(s.POIs) > 0 { 71 | fmt.Fprintf(buf, format.Header(t, 3, "Points of Interest")) 72 | for _, p := range s.POIs { 73 | fmt.Fprintln(buf, p.Format(t)) 74 | } 75 | } 76 | 77 | return buf.String() 78 | } 79 | 80 | // Stars represents the generated collection of Stars that will be used to populate a hex grid 81 | type Stars struct { 82 | Rows, Cols int 83 | Systems []*Star 84 | } 85 | 86 | // Density of star systems in a sector 87 | type Density int 88 | 89 | // Consts for star density 90 | const ( 91 | SPARSE Density = iota 92 | AVERAGE 93 | DENSE 94 | ) 95 | 96 | // NewSector returns a blank Sector struct and generates tag information according to the guidelines 97 | // in pages 133 - 177 of Stars Without Number (Revised Edition). 98 | func NewSector(rows, cols int, excludeTags []string, fullTags bool, poiChance, otherWorldChance int, density Density) *Stars { 99 | s := &Stars{ 100 | Rows: rows, 101 | Cols: cols, 102 | } 103 | 104 | dVal := 0 105 | switch density { 106 | case SPARSE: 107 | dVal = 8 108 | case AVERAGE: 109 | dVal = 4 110 | case DENSE: 111 | dVal = 2 112 | } 113 | 114 | cells := s.Rows * s.Cols 115 | stars := (rand.Intn(cells/4) / 2) + (cells / dVal) 116 | 117 | for row, col := rand.Intn(s.Rows), rand.Intn(s.Cols); len(s.Systems) <= stars; row, col = rand.Intn(s.Rows), rand.Intn(s.Cols) { 118 | if !s.active(row, col) { 119 | s.Systems = append(s.Systems, NewStar(row, col, s.systemName(), excludeTags, fullTags, poiChance, otherWorldChance)) 120 | } 121 | } 122 | 123 | return s 124 | } 125 | 126 | // UniqueName ensures rolls on the name.System table until it gets a name that is not currently in use. 127 | func (s *Stars) systemName() string { 128 | //n := name.System.Roll() // Try system first 129 | n := name.Generate(rand.Intn(4) + 3) 130 | for { 131 | if !s.nameUsed(n) { 132 | return n 133 | } 134 | 135 | n = name.Generate(rand.Intn(4) + 3) 136 | } 137 | } 138 | 139 | func (s *Stars) nameUsed(n string) bool { 140 | for _, star := range s.Systems { 141 | if star.Name == n { 142 | return true 143 | } 144 | } 145 | 146 | return false 147 | } 148 | 149 | // active checks to see if there is an active Star at r(ow), c(ol) 150 | func (s *Stars) active(row, col int) bool { 151 | for _, star := range s.Systems { 152 | if star.Row == row && star.Col == col { 153 | return true 154 | } 155 | } 156 | 157 | return false 158 | } 159 | -------------------------------------------------------------------------------- /content/alien.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | 9 | "github.com/nboughton/go-roll" 10 | "github.com/nboughton/swnt/content/format" 11 | "github.com/nboughton/swnt/content/table" 12 | "github.com/nboughton/swnt/dice" 13 | ) 14 | 15 | func init() { 16 | table.Registry.Add(alienTable.body) 17 | table.Registry.Add(alienTable.socialStructure) 18 | } 19 | 20 | // Alien with a Body 21 | type Alien struct { 22 | Body string 23 | Lense string 24 | SocialStructure string 25 | } 26 | 27 | // NewAlien with random characteristics 28 | func NewAlien() Alien { 29 | a := Alien{ 30 | Body: alienTable.body.Roll(), 31 | Lense: alienTable.lense.Roll(), 32 | SocialStructure: alienTable.socialStructure.Roll(), 33 | } 34 | return a 35 | } 36 | 37 | // Format returns Alien a formatted as type t 38 | func (a Alien) Format(t format.OutputType) string { 39 | buf := new(bytes.Buffer) 40 | 41 | fmt.Fprintf(buf, format.Table(t, []string{"Alien", ""}, [][]string{ 42 | {alienTable.body.Name, a.Body}, 43 | {alienTable.lense.Name, a.Lense}, 44 | {alienTable.socialStructure.Name, a.SocialStructure}, 45 | })) 46 | 47 | return buf.String() 48 | } 49 | 50 | func (a Alien) String() string { 51 | return a.Format(format.TEXT) 52 | } 53 | 54 | var alienTable = struct { 55 | body roll.Table 56 | lense roll.Table 57 | socialStructure roll.Table 58 | }{ 59 | // Body SWN Revised Free Edition p203 60 | roll.Table{ 61 | Name: "Body", 62 | ID: "alien.Body", 63 | Dice: roll.Dice{N: 1, Die: roll.D6}, 64 | Items: []roll.TableItem{ 65 | {Match: []int{1}, Text: "Avian, bat-like, pterodactylian"}, 66 | {Match: []int{2}, Text: "Reptilian, amphibian, draconic"}, 67 | {Match: []int{3}, Text: "Insectile, beetle-like, spiderish, wasp-like"}, 68 | {Match: []int{4}, Text: "Mammalian, furred or bare-skinned"}, 69 | {Match: []int{5}, Text: "Exotic, composed of some novel substance"}, 70 | {Match: []int{6}, Text: "Hybrid of two or more types", Action: func() string { 71 | tbl, _ := table.Registry.Get("alien.Body") 72 | tbl.Dice = roll.Dice{N: 1, Die: dice.D5} 73 | 74 | types, res := 2+rand.Intn(4), make(map[string]bool) 75 | for len(res) < types { 76 | res[tbl.Roll()] = true 77 | } 78 | 79 | text := []string{} 80 | for k := range res { 81 | text = append(text, k) 82 | } 83 | 84 | return "\n\t\t" + strings.Join(text, "\n\t\t") 85 | }}, 86 | }, 87 | }, 88 | 89 | // Lense from SWN Revised Free Edition p205 90 | roll.Table{ 91 | Name: "Lense", 92 | Dice: roll.Dice{N: 1, Die: roll.D20}, 93 | Items: []roll.TableItem{ 94 | {Match: []int{1}, Text: "Collectivity"}, 95 | {Match: []int{2}, Text: "Curiosity"}, 96 | {Match: []int{3}, Text: "Despair"}, 97 | {Match: []int{4}, Text: "Dominion"}, 98 | {Match: []int{5}, Text: "Faith"}, 99 | {Match: []int{6}, Text: "Fear"}, 100 | {Match: []int{7}, Text: "Gluttony"}, 101 | {Match: []int{8}, Text: "Greed"}, 102 | {Match: []int{9}, Text: "Hate"}, 103 | {Match: []int{10}, Text: "Honor"}, 104 | {Match: []int{11}, Text: "Journeying"}, 105 | {Match: []int{12}, Text: "Joy"}, 106 | {Match: []int{13}, Text: "Pacifism"}, 107 | {Match: []int{14}, Text: "Pride"}, 108 | {Match: []int{15}, Text: "Sagacity"}, 109 | {Match: []int{16}, Text: "Subtlety"}, 110 | {Match: []int{17}, Text: "Tradition"}, 111 | {Match: []int{18}, Text: "Treachery"}, 112 | {Match: []int{19}, Text: "Tribalism"}, 113 | {Match: []int{20}, Text: "Wrath"}, 114 | }, 115 | }, 116 | 117 | // SocialStructure from SWN Revised Free Edition p207 118 | roll.Table{ 119 | Name: "Social Structure", 120 | ID: "alien.SocialStructure", 121 | Dice: roll.Dice{N: 1, Die: roll.D8}, 122 | Items: []roll.TableItem{ 123 | {Match: []int{1}, Text: "Democratic"}, 124 | {Match: []int{2}, Text: "Monarchic"}, 125 | {Match: []int{3}, Text: "Tribal"}, 126 | {Match: []int{4}, Text: "Oligarchic"}, 127 | {Match: []int{5, 6}, Text: "Multipolar Competitive", Action: actionSocialStructure}, 128 | {Match: []int{7, 8}, Text: "Multipolar Cooperative", Action: actionSocialStructure}, 129 | }, 130 | }, 131 | } 132 | 133 | func actionSocialStructure() string { 134 | tbl, _ := table.Registry.Get("alien.SocialStructure") 135 | tbl.Dice = roll.Dice{N: 1, Die: roll.D4} 136 | 137 | types, res := 2+rand.Intn(3), make(map[string]bool) 138 | for len(res) < types { 139 | res[tbl.Roll()] = true 140 | } 141 | 142 | text := []string{} 143 | for k := range res { 144 | text = append(text, k) 145 | } 146 | 147 | return strings.Join(text, ", ") 148 | } 149 | -------------------------------------------------------------------------------- /content/religion.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/nboughton/go-roll" 9 | "github.com/nboughton/swnt/content/format" 10 | "github.com/nboughton/swnt/content/table" 11 | "github.com/nboughton/swnt/dice" 12 | ) 13 | 14 | func init() { 15 | table.Registry.Add(religionTable.leadership) 16 | } 17 | 18 | // Religion is pretty self explanatory 19 | type Religion struct { 20 | Evolution string 21 | Leadership string 22 | OriginTradition string 23 | } 24 | 25 | // NewReligion with random characteristics 26 | func NewReligion() Religion { 27 | r := Religion{ 28 | Evolution: religionTable.evolution.Roll(), 29 | Leadership: religionTable.leadership.Roll(), 30 | OriginTradition: religionTable.origin.Roll(), 31 | } 32 | return r 33 | } 34 | 35 | // Format returns the religion formatted as type t 36 | func (r Religion) Format(t format.OutputType) string { 37 | buf := new(bytes.Buffer) 38 | 39 | fmt.Fprintf(buf, format.Table(t, []string{"Religion", ""}, [][]string{ 40 | {religionTable.origin.Name, r.OriginTradition}, 41 | {religionTable.evolution.Name, r.Evolution}, 42 | {religionTable.leadership.Name, r.Leadership}, 43 | })) 44 | 45 | return buf.String() 46 | } 47 | 48 | func (r Religion) String() string { 49 | return r.Format(format.TEXT) 50 | } 51 | 52 | /*************** TABLES ***************/ 53 | 54 | var religionTable = struct { 55 | evolution roll.List 56 | origin roll.List 57 | leadership roll.Table 58 | }{ 59 | // Evolution SWN Revised Free Edition p193 60 | roll.List{ 61 | Name: "Evolution", 62 | Items: []string{ 63 | "New holy book. Someone in the faith’s past penned or discovered a text that is now taken to be holy writ and the expressed will of the divine.", 64 | "New prophet. This faith reveres the words and example of a relatively recent prophet, esteeming him or her as the final word on the will of God. The prophet may or may not still be living.", 65 | "Syncretism. The faith has merged many of its beliefs with another religion. Roll again on the origin tradition table; this faith has reconciled the major elements of both beliefs into its tradition.", 66 | "Neofundamentalism. The faith is fiercely resistant to perceived innovations and deviations from their beliefs. Even extremely onerous traditions and restrictions will be observed to the letter.", 67 | "Quietism. The faith shuns the outside world and involvement with the affairs of nonbelievers. They prefer to keep to their own kind and avoid positions of wealth and power.", 68 | "Sacrifices. The faith finds it necessary to make substantial sacrifices to please God. Some faiths may go so far as to offer human sacrifices, while others insist on huge tithes offered to the building of religious edifices.", 69 | "Schism. The faith’s beliefs are actually almost identical to those of the majority of its origin tradition, save for a few minor points of vital interest to theologians and no practical difference whatsoever to believers. This does not prevent a burning resentment towards the parent faith.", 70 | "Holy family. God’s favor has been shown especially to members of a particular lineage. It may be that only men and women of this bloodline are permitted to become clergy, or they may serve as helpless figureheads for the real leaders of the faith", 71 | }, 72 | }, 73 | 74 | // OriginTradition SWN Revised Free Edition p193 75 | roll.List{ 76 | Name: "Origin", 77 | Items: []string{ 78 | "Paganism", 79 | "Roman Catholicism", 80 | "Eastern Orthodox Christianity", 81 | "Protestant Christianity", 82 | "Buddhism", 83 | "Judaism", 84 | "Islam", 85 | "Taoism", 86 | "Hinduism", 87 | "Zoroastrianism", 88 | "Confucianism", 89 | "Ideology", 90 | }, 91 | }, 92 | 93 | // Leadership SWN Revised Free Edition p193 94 | roll.Table{ 95 | Name: "Leadership", 96 | ID: "religion.Leadership", 97 | Dice: roll.Dice{N: 1, Die: roll.D6}, 98 | Items: []roll.TableItem{ 99 | {Match: []int{1, 2}, Text: "Patriarch/Matriarch. A single leader determines doctrine for the entire religion, possibly in consultation with other clerics."}, 100 | {Match: []int{3, 4}, Text: "Council. A group of the oldest and most revered clergy determine the course of the faith."}, 101 | {Match: []int{5}, Text: "Democracy. Every member has an equal voice in matters of faith, with doctrine usually decided at regular church- wide councils."}, 102 | {Match: []int{6}, Text: "No universal leadership", Action: func() string { 103 | tbl, _ := table.Registry.Get("religion.Leadership") 104 | tbl.Dice = roll.Dice{N: 1, Die: dice.D5} 105 | 106 | if rand.Intn(6)+1 == 6 { 107 | return "" 108 | } 109 | 110 | return "Each region governed independently by a " + tbl.Roll() 111 | }}, 112 | }, 113 | }, 114 | } 115 | -------------------------------------------------------------------------------- /content/corporation.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/nboughton/go-roll" 8 | "github.com/nboughton/swnt/content/format" 9 | ) 10 | 11 | // Corporation with a Body 12 | type Corporation struct { 13 | Name string 14 | Organization string 15 | Business string 16 | ReputationAndRumor string 17 | } 18 | 19 | // NewCorporation with random characteristics 20 | func NewCorporation() Corporation { 21 | c := Corporation{ 22 | Name: corpTable.name.Roll(), 23 | Organization: corpTable.organization.Roll(), 24 | Business: corpTable.business.Roll(), 25 | ReputationAndRumor: corpTable.reputation.Roll(), 26 | } 27 | return c 28 | } 29 | 30 | // Format returns Corporation c formatted as type t 31 | func (c Corporation) Format(t format.OutputType) string { 32 | buf := new(bytes.Buffer) 33 | 34 | fmt.Fprintf(buf, format.Table(t, []string{"Corporation", ""}, [][]string{ 35 | {corpTable.name.Name, fmt.Sprintf("%s %s", c.Name, c.Organization)}, 36 | {corpTable.business.Name, c.Business}, 37 | {corpTable.reputation.Name, c.ReputationAndRumor}, 38 | })) 39 | 40 | return buf.String() 41 | } 42 | 43 | func (c Corporation) String() string { 44 | return c.Format(format.TEXT) 45 | } 46 | 47 | var corpTable = struct { 48 | name roll.List 49 | organization roll.List 50 | business roll.List 51 | reputation roll.List 52 | }{ 53 | // Name SWN Revised Free Edition p192 54 | roll.List{ 55 | Name: "Name", 56 | Items: []string{ 57 | "Ad Astra", 58 | "Colonial", 59 | "Compass", 60 | "Daybreak", 61 | "Frontier", 62 | "Guo Yin", 63 | "Highbeam", 64 | "Imani", 65 | "Magnus", 66 | "Meteor", 67 | "Neogen", 68 | "New Dawn", 69 | "Omnitech", 70 | "Outertech", 71 | "Overwatch", 72 | "Panstellar", 73 | "Shogun", 74 | "Silverlight", 75 | "Spiker", 76 | "Stella", 77 | "Striker", 78 | "Sunbeam", 79 | "Terra Prime", 80 | "Wayfarer", 81 | "West Wind", 82 | }, 83 | }, 84 | 85 | // Organization SWN Revised Free Edition p192 86 | roll.List{ 87 | Name: "Organization", 88 | Items: []string{ 89 | "Alliance", 90 | "Association", 91 | "Band", 92 | "Circle", 93 | "Clan", 94 | "Combine", 95 | "Company", 96 | "Cooperative", 97 | "Corporation", 98 | "Enterprises", 99 | "Faction", 100 | "Group", 101 | "Megacorp", 102 | "Multistellar", 103 | "Organization", 104 | "Outfit", 105 | "Pact", 106 | "Partnership", 107 | "Ring", 108 | "Society", 109 | "Sodality", 110 | "Syndicate", 111 | "Union", 112 | "Unity", 113 | "Zaibatsu", 114 | }, 115 | }, 116 | 117 | // Business SWN Revised Free Edition p192 118 | roll.List{ 119 | Name: "Business", 120 | Items: []string{ 121 | "Aeronautics", 122 | "Agriculture", 123 | "Art", 124 | "Assassination", 125 | "Asteroid Mining", 126 | "Astrotech", 127 | "Biotech", 128 | "Bootlegging", 129 | "Computer Hardware", 130 | "Construction", 131 | "Cybernetics", 132 | "Electronics", 133 | "Energy Weapons", 134 | "Entertainment", 135 | "Espionage", 136 | "Exploration", 137 | "Fishing", 138 | "Fuel Refining", 139 | "Gambling", 140 | "Gemstones", 141 | "Gengineering", 142 | "Grav Vehicles", 143 | "Heavy Weapons", 144 | "Ideology", 145 | "Illicit Drugs", 146 | "Journalism", 147 | "Law Enforcement", 148 | "Liquor", 149 | "Livestock", 150 | "Maltech", 151 | "Mercenary Work", 152 | "Metallurgy", 153 | "Pharmaceuticals", 154 | "Piracy", 155 | "Planetary Mining", 156 | "Plastics", 157 | "Pretech", 158 | "Prisons", 159 | "Programming", 160 | "Projectile Guns", 161 | "Prostitution", 162 | "Psionics", 163 | "Psitech", 164 | "Robotics", 165 | "Security", 166 | "Shipyards", 167 | "Snacks", 168 | "Telcoms", 169 | "Transport", 170 | "Xenotech", 171 | }, 172 | }, 173 | 174 | // ReputationAndRumor SWN Revised Free Edition p192 175 | roll.List{ 176 | Name: "Reputation and Rumors", 177 | Items: []string{ 178 | "Reckless with the lives of their employees", 179 | "Have a dark secret about their board of directors", 180 | "Notoriously xenophobic towards aliens", 181 | "Lost much money to an embezzler who evaded arrest", 182 | "Reliable and trustworthy goods", 183 | "Stole a lot of R&D from a rival corporation", 184 | "They have high-level political connections", 185 | "Rumored cover-up of a massive industrial accident", 186 | "Stodgy and very conservative in their business plans", 187 | "Stodgy and very conservative in their business plans", 188 | "The company’s owner is dangerously insane", 189 | "Rumored ties to a eugenics cult", 190 | "Said to have a cache of pretech equipment", 191 | "Possibly teetering on the edge of bankruptcy", 192 | "Front for a planetary government’s espionage arm", 193 | "Secretly run by a psychic cabal", 194 | "Secretly run by hostile aliens", 195 | "Secretly run by an unbraked AI", 196 | "They’ve turned over a new leaf with the new CEO", 197 | "Deeply entangled with the planetary underworld", 198 | }, 199 | }, 200 | } 201 | -------------------------------------------------------------------------------- /haxscii/haxscii.go: -------------------------------------------------------------------------------- 1 | // Package haxscii is a simple libary that overlays cell text over a generated ascii hex map 2 | package haxscii 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | type colourFunc func(string, ...interface{}) string 13 | 14 | var ( 15 | offset = 5 16 | ) 17 | 18 | // Colour funcs 19 | var ( 20 | White = color.WhiteString 21 | Red = color.RedString 22 | Yellow = color.YellowString 23 | Magenta = color.MagentaString 24 | Green = color.GreenString 25 | Blue = color.BlueString 26 | Cyan = color.CyanString 27 | ) 28 | 29 | func genCrdText(row, col int) string { 30 | rStr, cStr := strconv.Itoa(row), strconv.Itoa(col) 31 | if row < 10 { 32 | rStr = "0" + rStr 33 | } 34 | if col < 10 { 35 | cStr = "0" + cStr 36 | } 37 | 38 | return fmt.Sprintf("%s,%s", rStr, cStr) 39 | } 40 | 41 | type cell struct { 42 | text [][]string 43 | widthTop int 44 | widthMid int 45 | height int 46 | crdsRow int // Set the row/col to align the coords to 47 | crdsCol int 48 | } 49 | 50 | /* A cell: 51 | ` \__________/ 52 | /r \ 53 | / \ 54 | / \ 55 | \ / 56 | \ / 57 | \__________/ `, 58 | */ 59 | 60 | func newCell(row, col int) *cell { 61 | c := &cell{ 62 | text: [][]string{ 63 | {" ", " ", `\`, "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "/", " ", " "}, 64 | {" ", " ", "/", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", `\`, " ", " "}, 65 | {" ", "/", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", `\`, " "}, 66 | {"/", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", `\`}, 67 | {`\`, " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "/"}, 68 | {" ", `\`, " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "/", " "}, 69 | {" ", " ", `\`, "_", "_", "_", "_", "_", "_", "_", "_", "_", "_", "/", " ", " "}, 70 | }, 71 | height: 7, 72 | widthTop: 10, 73 | widthMid: 16, 74 | crdsRow: 1, 75 | crdsCol: 3, 76 | } 77 | 78 | c.setCrds(row, col) 79 | 80 | return c 81 | } 82 | 83 | func (c *cell) setCrds(row, col int) { 84 | for i, sub := range genCrdText(row, col) { 85 | c.text[c.crdsRow][c.crdsCol+i] = string(sub) 86 | } 87 | } 88 | 89 | // Map represents a 2 dimensional string matrix of the template 90 | type Map [][]string 91 | 92 | // NewMap generates a mapscii template so that text can be superimposed on it 93 | func NewMap(height, width int) Map { 94 | var ( 95 | cl = newCell(0, 0) // Use a cell as a reference 96 | wDiff = (cl.widthMid - cl.widthTop) / 2 97 | w = (width * (cl.widthMid - wDiff)) + wDiff 98 | h = (height * (cl.height - 1)) + cl.height/2 + 1 // Shared borders reduce total height 99 | m = make(Map, h) 100 | ) 101 | 102 | // Create blank template space 103 | for r := 0; r < h; r++ { 104 | m[r] = make([]string, w) 105 | for c := range m[r] { 106 | m[r][c] = " " 107 | } 108 | } 109 | 110 | for r := 0; r < height; r++ { 111 | row := r * (cl.height - 1) 112 | 113 | for c := 0; c < width; c++ { 114 | col := c * (cl.widthMid - 3) 115 | if c%2 != 0 { 116 | row = r*(cl.height-1) + cl.height/2 117 | } 118 | 119 | m.emptyCell(row, col, r, c) 120 | if c%2 != 0 { 121 | row = row - cl.height/2 122 | } 123 | } 124 | } 125 | 126 | return m 127 | } 128 | 129 | // emptyCell writes a blank cell to the Map matrix 130 | func (m Map) emptyCell(row, col, rLabel, cLabel int) Map { 131 | r, c, cl := row, col, newCell(rLabel, cLabel) 132 | 133 | for cellRow := 0; cellRow < cl.height; cellRow++ { 134 | for _, char := range cl.text[cellRow] { 135 | if r >= len(m) || c >= len(m[r]) { // Bounds check matrices references 136 | return m 137 | } 138 | 139 | m[r][c] = char 140 | c++ 141 | } 142 | r++ 143 | c = col 144 | } 145 | 146 | return m 147 | } 148 | 149 | // SetTxt sets the text of a given hex 150 | func (m Map) SetTxt(row, col int, lines [4]string, color colourFunc) { 151 | // Define row and column based on the same calculations used to place blank 152 | // cells 153 | var ( 154 | cl = newCell(0, 0) 155 | wDiff = (cl.widthMid - cl.widthTop) / 2 156 | r = row*(cl.height-1) + cl.crdsRow 157 | c = col*(cl.widthMid-wDiff) + cl.crdsCol 158 | ) 159 | 160 | if col%2 != 0 { 161 | r += cl.height / 2 162 | } 163 | 164 | for i, line := range lines { 165 | m.print(r+i+1, c+offset-(len(line)/2), line, color) 166 | } 167 | } 168 | 169 | func (m Map) print(startRow, startCol int, text string, colour colourFunc) { 170 | for row, col, i := startRow, startCol, 0; i < len(text); col, i = col+1, i+1 { 171 | if col < 0 { 172 | col = 0 173 | } 174 | 175 | if col < len(m[row]) { 176 | m[row][col] = colour(string(text[i])) 177 | } else { 178 | m[row] = append(m[row], colour(string(text[i]))) 179 | } 180 | } 181 | } 182 | 183 | func (m Map) String() string { 184 | s := "" 185 | 186 | for _, line := range m { 187 | s += strings.Join(line, "") + "\n" 188 | } 189 | 190 | return s 191 | } 192 | 193 | // Colour toggles the colour output on/off (true/false) 194 | func Colour(b bool) { 195 | color.NoColor = !b 196 | } 197 | -------------------------------------------------------------------------------- /cmd/sector.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Nick Boughton 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "log" 26 | "os" 27 | "strings" 28 | 29 | "github.com/nboughton/swnt/content/name" 30 | "github.com/nboughton/swnt/content/sector" 31 | "github.com/nboughton/swnt/export" 32 | "github.com/spf13/cobra" 33 | ) 34 | 35 | var ( 36 | dirPerm = os.FileMode(0755) 37 | filePerm = os.FileMode(0644) 38 | ) 39 | 40 | // sectorCmd represents the sector command 41 | var sectorCmd = &cobra.Command{ 42 | Use: "sector", 43 | Short: "Create the skeleton of a Sector", 44 | Long: ``, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | // get flags 47 | var ( 48 | excludeTags, _ = cmd.Flags().GetStringArray(flExclude) 49 | fullTags, _ = cmd.Flags().GetBool(flLongTags) 50 | poiChance, _ = cmd.Flags().GetInt(flPoi) 51 | otherWorldChance, _ = cmd.Flags().GetInt(flOW) 52 | secHeight, _ = cmd.Flags().GetInt(flSecHeight) 53 | secWidth, _ = cmd.Flags().GetInt(flSecWidth) 54 | exportTypes, _ = cmd.Flags().GetString(flExport) 55 | density, _ = cmd.Flags().GetString(flDensity) 56 | ) 57 | 58 | dVal := sector.AVERAGE 59 | switch density { 60 | case "sparse": 61 | dVal = sector.SPARSE 62 | case "average": 63 | dVal = sector.AVERAGE 64 | case "dense": 65 | dVal = sector.DENSE 66 | default: 67 | fmt.Printf("Unknown density value [%s], use sparse, average or dense\n", density) 68 | return 69 | } 70 | 71 | if secHeight < 2 || secHeight > 99 || secWidth < 2 || secWidth > 99 { 72 | fmt.Println("Sectors larger than 99, or smaller than 2, hexes in either direction are not supported") 73 | return 74 | } 75 | 76 | var ( 77 | secData = sector.NewSector(secHeight, secWidth, excludeTags, fullTags, poiChance, otherWorldChance, dVal) 78 | secName = genSectorName() 79 | ) 80 | 81 | fmt.Println(secName) 82 | fmt.Println(export.Hexmap(secData, true, false)) 83 | 84 | ans := "r" 85 | for { 86 | fmt.Printf("Write Sector? [y]es, [n]o, [r]eroll: [%s] ", ans) 87 | fmt.Scanf("%s", &ans) 88 | switch ans { 89 | case "y": 90 | if err := os.Mkdir(secName, dirPerm); err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | if err := os.Chdir(secName); err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | for _, t := range strings.Split(exportTypes, ",") { 99 | if exporter, err := export.New(t, secName, secData); exporter != nil { 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | 104 | if err = exporter.Write(); err != nil { 105 | log.Fatal(err) 106 | } 107 | } 108 | } 109 | 110 | return 111 | 112 | case "n": 113 | return 114 | 115 | case "r": 116 | secData = sector.NewSector(secHeight, secWidth, excludeTags, fullTags, poiChance, otherWorldChance, dVal) 117 | secName = genSectorName() 118 | fmt.Println(secName) 119 | fmt.Println(export.Hexmap(secData, true, false)) 120 | } 121 | } 122 | }, 123 | } 124 | 125 | func genSectorName() string { 126 | secName := fmt.Sprintf("%s Sector", name.System.Roll()) 127 | _, err := os.Stat(secName) // Ensure that there isn't already a sector of this name in the working directory 128 | for os.IsExist(err) { 129 | secName = fmt.Sprintf("%s Sector", name.System.Roll()) 130 | _, err = os.Stat(secName) 131 | } 132 | 133 | return secName 134 | } 135 | 136 | func init() { 137 | newCmd.AddCommand(sectorCmd) 138 | sectorCmd.Flags().StringArrayP(flExclude, "x", []string{}, "Exclude tags (-x zombies -x \"regional hegemon\" etc)") 139 | sectorCmd.Flags().BoolP(flLongTags, "l", false, "Toggle full world tag info in output") 140 | sectorCmd.Flags().IntP(flPoi, "p", 40, "Set % chance of a POI being generated for any given star in the sector") 141 | sectorCmd.Flags().IntP(flOW, "o", 15, "Set % chance for a secondary world to be generated for any given star in the sector") 142 | sectorCmd.Flags().IntP(flSecHeight, "e", 10, "Set height of sector in hexes") 143 | sectorCmd.Flags().IntP(flSecWidth, "w", 8, "Set width of sector in hexes") 144 | sectorCmd.Flags().String(flExport, "txt,json", "Set export formats. Format types must be comma separated without spaces. Supported formats are txt, json and hugo") 145 | sectorCmd.Flags().StringP(flDensity, "d", "average", "Set star density in sector. Options are sparse, average or dense") 146 | } 147 | -------------------------------------------------------------------------------- /content/heresy.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/nboughton/go-roll" 8 | "github.com/nboughton/swnt/content/format" 9 | ) 10 | 11 | // Heresy with a Body 12 | type Heresy struct { 13 | Founder string 14 | MajorHeresy string 15 | Attitude string 16 | Quirk string 17 | } 18 | 19 | // NewHeresy with random characteristics 20 | func NewHeresy() Heresy { 21 | h := Heresy{ 22 | Founder: heresyTable.founder.Roll(), 23 | MajorHeresy: heresyTable.majorHeresy.Roll(), 24 | Attitude: heresyTable.attitude.Roll(), 25 | Quirk: heresyTable.quirk.Roll(), 26 | } 27 | return h 28 | } 29 | 30 | // Format return Heresy h as type t 31 | func (h Heresy) Format(t format.OutputType) string { 32 | buf := new(bytes.Buffer) 33 | 34 | fmt.Fprintf(buf, format.Table(t, []string{"Heresy", ""}, [][]string{ 35 | {heresyTable.founder.Name, h.Founder}, 36 | {heresyTable.majorHeresy.Name, h.MajorHeresy}, 37 | {heresyTable.attitude.Name, h.Attitude}, 38 | {heresyTable.quirk.Name, h.Quirk}, 39 | })) 40 | 41 | return buf.String() 42 | } 43 | 44 | func (h Heresy) string() string { 45 | return h.Format(format.TEXT) 46 | } 47 | 48 | var heresyTable = struct { 49 | founder roll.List 50 | majorHeresy roll.List 51 | attitude roll.List 52 | quirk roll.List 53 | }{ 54 | // Founder SWN Revised Free Edition p194 55 | roll.List{ 56 | Name: "Founder", 57 | Items: []string{ 58 | "Defrocked clergy: founded by a cleric outcast from the faith.", 59 | "Frustrated layman: founded by a layman frustrated with the faith’s decadence, rigidity, or lack of authenticity", 60 | "Renegade prophet: founded by a revered holy figure who broke with the faith", 61 | "High prelate: founded by an important and powerful cleric to convey his or her beliefs", 62 | "Dissatisfied minor clergy: founded by a minor cleric frustrated with the faith’s current condition", 63 | "Outsider: founded by a member of another faith deeply influenced by the parent religion", 64 | "Academic: founded by a professor or theologian on intellectual grounds", 65 | "Accidental; the founder never meant their works to be taken that way.", 66 | }, 67 | }, 68 | 69 | // MajorHeresy SWN Revised Free Edition p194 70 | roll.List{ 71 | Name: "Major Heresy", 72 | Items: []string{ 73 | "Manichaeanism: the sect believes in harsh austerities and rejection of matter as something profane and evil", 74 | "Donatism: the sect believes that clergy must be personally pure and holy in order to be legitimate", 75 | "Supercessionism: the sect believes the founder or some other source supercedes former scripture or tradition", 76 | "Antinomianism: the sect believes that their holy persons are above any earthly law and may do as they will", 77 | "Universal priesthood: the sect believes that there is no distinction between clergy and layman and that all functions of the faith may be performed by all members", 78 | "Conciliarism: the sect believes that the consensus of believers may correct or command even the clerical leadership of the faith", 79 | "Ethnocentrism: the sect believes that only a particular ethnicity or nationality can truly belong to the faith", 80 | "Separatism: the sect believes members should shun involvement with the secular world", 81 | "Stringency: the sect believes that even minor sins should be punished, and major sins should be capital crimes", 82 | "Syncretism: the sect has added elements of another native faith to their beliefs", 83 | "Primitivism: the sect tries to recreate what they imagine was the original community of faith", 84 | "Conversion by the sword: unbelievers must be brought to obedience to the sect or be granted death", 85 | }, 86 | }, 87 | 88 | // Attitude SWN Revised Free Edition p194 89 | roll.List{ 90 | Name: "Attitude", 91 | Items: []string{ 92 | "Filial: the sect honors and respects the orthodox faith, but feels it is substantially in error", 93 | "Anathematic: the orthodox are spiritually worse than infidels, and their ways must be avoided at all costs", 94 | "Evangelical: the sect feels compelled to teach the orthodox the better truth of their ways", 95 | "Contemptuous: the orthodox are spiritually lost and ignoble", 96 | "Aversion: the sect wishes to shun and avoid the orthodox", 97 | "Hatred: the sect wishes the death or conversion of the orthodox", 98 | "Indifference: the sect has no particular animus or love for the orthodox", 99 | "Obedience: the sect feels obligated to obey the orthodox hierarchy in all matters not related to their specific faith", 100 | "Legitimist: the sect views itself as the \"true\" orthodox faith and the present orthodox hierarchy as pretenders to their office", 101 | "Purificationist: the sect’s austerities, sufferings, and asceticisms are necessary to purify the orthodox", 102 | }, 103 | }, 104 | 105 | // Quirk SWN Revised Free Edition p194 106 | roll.List{ 107 | Name: "Quirk", 108 | Items: []string{ 109 | "Clergy of only one gender", 110 | "Dietary prohibitions", 111 | "Characteristic item of clothing or jewelry", 112 | "Public prayer at set times or places", 113 | "Forbidden to do something commonly done", 114 | "Anti-intellectual, deploring secular learning", 115 | "Mystical, seeking union with God through meditation", 116 | "Lives in visibly distinct houses or districts", 117 | "Has a language specific to the sect", 118 | "Greatly respects learning and education", 119 | "Favors specific colors or symbols", 120 | "Has unique purification rituals", 121 | "Always armed", 122 | "Forbids marriage or romance outside the sect", 123 | "Will not eat with people outside the sect", 124 | "Must donate labor or tithe money to the sect", 125 | "Special friendliness toward another faith or ethnicity", 126 | "Favors certain professions for their membership", 127 | "Vigorous charity work among unbelievers", 128 | "Forbidden the use of certain technology", 129 | }, 130 | }, 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swnt - Command line tools for Stars Without Number GMs 2 | 3 | swnt provides a command line interface to the roll tables and generators found in [Stars Without Number : Revised Edition (free version)](https://www.drivethrurpg.com/product/230009/Stars-Without-Number-Revised-Edition-Free-Version). 4 | While the code is MIT licensed, all roll table content (except the name.System table) is the copyright of [Kevin Crawford, Sine Nominee Publishing](https://sinenominepublishing.com/). 5 | 6 | ## Features 7 | 8 | * Generate sectors up to 99x99 hexes 9 | * Export sectors as 10 | * * plain text (with directory structure) 11 | * * a hugo site using a fork of the docdock theme. This includes indexing and text search support 12 | * * JSON 13 | * Has generators for pretty much all tables in the Free edition of Stars Without Number (I don't think I missed any, let me know if I did) 14 | 15 | ## Installation 16 | 17 | ### Arch Linux 18 | 19 | swnt is available from the AUR at https://aur.archlinux.org/packages/swnt/ or via your favourite AUR helper. Personally I use yay. 20 | 21 | ``` 22 | yay -Syu swnt 23 | ``` 24 | 25 | ### Other Linux Distributions 26 | 27 | You can build swnt yourself or use the binary release from the [releases page](https://github.com/nboughton/swnt/releases). 28 | 29 | **Building from source** 30 | 31 | You will need the [go compiler](https://golang.org/) to install swnt. Once installed just run 32 | 33 | go get github.com/nboughton/swnt 34 | go install github.com/nboughton/swnt 35 | 36 | swnt should build on other platforms but I'm not able to test them so I can't guarantee it'll work as expected on anything other than linux. 37 | 38 | To make full use of swnt's hugo export function for generated sectors you'll also need [Hugo](https://gohugo.io) 39 | 40 | ## Usage 41 | 42 | swnt is mostly for rolling on tables and generating content such as sectors, npcs, points of interest, quick encounters etc using the "new" sub-command: 43 | 44 | ``` 45 | $ swnt new -h 46 | Generate content 47 | 48 | Usage: 49 | swnt new [command] 50 | 51 | Available Commands: 52 | adventure Generate an Adventure seed 53 | alien Generate an Alien 54 | beast Generate a Beast 55 | conflict Generate a Conflict/Problem 56 | corporation Generate a Corporation 57 | culture Generate a culture 58 | encounter Generate a quick encounter 59 | heresy Generate a Heresy 60 | npc Generate a NPC 61 | place Generate a place 62 | poi Generate a Point of Interest 63 | religion Generate a Religion 64 | sector Create the skeleton of a Sector 65 | world Generate a secondary World for a Sector cell 66 | 67 | Flags: 68 | -f, --format string Set output format. (--format txt,md). Not all commands support this flag. (default "txt") 69 | -h, --help help for new 70 | 71 | Use "swnt new [command] --help" for more information about a command. 72 | ``` 73 | 74 | However it's fairly like the command users will want to play with most is "new sector": 75 | 76 | ``` 77 | $ swnt new sector -h 78 | Create the skeleton of a Sector 79 | 80 | Usage: 81 | swnt new sector [flags] 82 | 83 | Flags: 84 | -d, --density string Set star density in sector. Options are sparse, average or dense (default "average") 85 | -x, --exclude stringArray Exclude tags (-x zombies -x "regional hegemon" etc) 86 | --export string Set export formats. Format types must be comma separated without spaces. Supported formats are txt, json and hugo (default "txt,json") 87 | -h, --help help for sector 88 | -l, --long-tags Toggle full world tag info in output 89 | -o, --other-worlds-chance int Set % chance for a secondary world to be generated for any given star in the sector (default 15) 90 | -p, --poi-chance int Set % chance of a POI being generated for any given star in the sector (default 40) 91 | -e, --sector-height int Set height of sector in hexes (default 10) 92 | -w, --sector-width int Set width of sector in hexes (default 8) 93 | 94 | Global Flags: 95 | -f, --format string Set output format. (--format txt,md). Not all commands support this flag. (default "txt") 96 | ``` 97 | 98 | Note new sector doesn't support the --format flag as export formats are covered by the export flag and output differently. 99 | 100 | Make sure you use a monospace font in your terminal otherwise the output won't line up properly. 101 | 102 | ![A generated sector](screenshot.png "A generated sector") 103 | 104 | All commands can be queried for their available options with the -h flag. For example: 105 | 106 | 107 | ``` 108 | $ swnt -h 109 | A simple application for generating content for Stars Without Number 110 | 111 | Usage: 112 | swnt [command] 113 | 114 | Available Commands: 115 | bestiary List creature statblocks 116 | help Help about any command 117 | new Generate content 118 | react Make a reaction roll for an NPC 119 | show Print the text of a world tag, table text will be added in future 120 | 121 | Flags: 122 | -h, --help help for swnt 123 | 124 | Use "swnt [command] --help" for more information about a command. 125 | ``` 126 | 127 | Most sub-commands of "new" (and the bestiary) support markdown as an output option with the -f (--format) flag. This makes it easier to copy and paste content straight into a Hugo exported sector. 128 | 129 | ## FAQ 130 | 131 | ### Why not make a web app? 132 | 133 | Because [Sectors Without Number](https://sectorswithoutnumber.com/) already exists and is an awesome tool. Also, I live on the CLI. It's where I work, where I'm comfortable and I suspect I'm not alone in that. swnt lets me just hammer a few keys and instantly get something back without having to faff around with GUIs and that's how I like it. I also like using screenshots of the player maps as my actual maps in Roll20. It's got that awful 80s retro-scifi look that just burns the retinas in all the right ways. 134 | -------------------------------------------------------------------------------- /content/bestiary.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/nboughton/swnt/content/format" 8 | ) 9 | 10 | // statBlock data for a beast/bot/npc 11 | type statBlock struct { 12 | Type string 13 | Name string 14 | HD int 15 | AC ac 16 | Atk atk 17 | Dmg string 18 | Move string 19 | ML int 20 | Skills int 21 | Saves int 22 | Cost int 23 | } 24 | 25 | // Format statBlock s as OutputType t 26 | func (s statBlock) Format(t format.OutputType) string { 27 | return format.Table(t, 28 | []string{"Type", "Name", "HD", "AC", "Atk", "Dmg", "Move", "ML", "Skills", "Saves", "Cost (robot/VI only)"}, 29 | [][]string{{s.Type, s.Name, fmt.Sprintf("%d", s.HD), s.AC.String(), s.Atk.String(), s.Dmg, s.Move, fmt.Sprintf("%d", s.ML), fmt.Sprintf("%d", s.Skills), fmt.Sprintf("%d", s.Saves), fmt.Sprintf("%d", s.Cost)}}, 30 | ) 31 | } 32 | 33 | func (s statBlock) String() string { 34 | return s.Format(format.TEXT) 35 | } 36 | 37 | type ac struct { 38 | Val int 39 | Notes string 40 | } 41 | 42 | func (a ac) String() string { 43 | n := "" 44 | if len(a.Notes) > 0 { 45 | n = fmt.Sprintf(" (%s)", a.Notes) 46 | } 47 | 48 | return fmt.Sprintf("%d%s", a.Val, n) 49 | } 50 | 51 | type atk struct { 52 | Val int 53 | X int 54 | } 55 | 56 | func (a atk) String() string { 57 | x := "" 58 | if a.X > 1 { 59 | x = fmt.Sprintf(" x %d", a.X) 60 | } 61 | 62 | return fmt.Sprintf("+%d%s", a.Val, x) 63 | } 64 | 65 | type statBlockTable []statBlock 66 | 67 | // Filter returns a filtered statblock using the terms supplied. Blocks are filtered on name and type. 68 | func (s statBlockTable) Filter(terms ...string) statBlockTable { 69 | if len(terms) == 0 { 70 | return s 71 | } 72 | 73 | out := statBlockTable{} 74 | 75 | for _, row := range s { 76 | for _, term := range terms { 77 | if strings.Contains(strings.ToLower(row.Name), strings.ToLower(term)) || strings.Contains(strings.ToLower(row.Type), strings.ToLower(term)) { 78 | out = append(out, row) 79 | break 80 | } 81 | } 82 | } 83 | 84 | return out 85 | } 86 | 87 | // Format StatBlock s as OutputType t 88 | func (s statBlockTable) Format(t format.OutputType) string { 89 | rows := [][]string{} 90 | 91 | for _, row := range s { 92 | rows = append(rows, []string{row.Type, row.Name, fmt.Sprintf("%d", row.HD), row.AC.String(), row.Atk.String(), row.Dmg, row.Move, fmt.Sprintf("%d", row.ML), fmt.Sprintf("%d", row.Skills), fmt.Sprintf("%d", row.Saves), fmt.Sprintf("%d", row.Cost)}) 93 | } 94 | 95 | return format.Table(t, []string{"Type", "Name", "HD", "AC", "Atk", "Dmg", "Move", "ML", "Skills", "Saves", "Cost (robot/VI only)"}, rows) 96 | } 97 | 98 | // StatBlocks for typical NPC combat encounters as per SWN pg195 99 | var StatBlocks = statBlockTable{ 100 | // Humans 101 | {"NPC", "Peaceful Human", 1, ac{10, ""}, atk{0, 1}, "Unarmed", "10m", 6, 1, 15, 0}, 102 | {"NPC", "Martial Human", 1, ac{10, ""}, atk{1, 1}, "By weapon", "10m", 8, 1, 15, 0}, 103 | {"NPC", "Veteran Fighter", 2, ac{14, ""}, atk{2, 1}, "By weapon +1", "10m", 9, 1, 14, 0}, 104 | {"NPC", "Elite Fighter", 3, ac{16, "combat"}, atk{4, 1}, "By weapon +1", "10m", 10, 2, 14, 0}, 105 | {"NPC", "Heroic Fighter", 6, ac{16, "combat"}, atk{8, 1}, "By weapon +3", "10m", 11, 3, 12, 0}, 106 | {"NPC", "Barbarian Hero", 6, ac{16, "primitive"}, atk{8, 1}, "By weapon +3", "10m", 11, 3, 12, 0}, 107 | {"NPC", "Barbarian Tribal", 1, ac{12, "primitive"}, atk{2, 1}, "By weapon", "10m", 8, 1, 15, 0}, 108 | {"NPC", "Gang Boss", 3, ac{14, ""}, atk{4, 1}, "By weapon +1", "10m", 9, 2, 14, 0}, 109 | {"NPC", "Gang Member", 1, ac{12, ""}, atk{1, 1}, "By weapon", "10m", 7, 1, 15, 0}, 110 | {"NPC", "Gengineered Killer", 4, ac{16, ""}, atk{5, 1}, "By weapon +1", "15m", 10, 2, 13, 0}, 111 | {"NPC", "Legendary Fighter", 10, ac{20, "powered"}, atk{12, 2}, "By weapon +4", "10m", 12, 5, 10, 0}, 112 | {"NPC", "Military Elite", 3, ac{16, "combat"}, atk{4, 1}, "By weapon +1", "10m", 10, 2, 14, 0}, 113 | {"NPC", "Military Soldier", 1, ac{16, "combat"}, atk{1, 1}, "By weapon", "10m", 9, 1, 15, 0}, 114 | {"NPC", "Normal Human", 1, ac{10, ""}, atk{0, 1}, "Unarmed", "10m", 6, 1, 15, 0}, 115 | {"NPC", "Pirate King", 7, ac{18, "powered"}, atk{9, 1}, "By weapon +2", "10m", 11, 3, 12, 0}, 116 | {"NPC", "Police Officer", 1, ac{14, ""}, atk{1, 1}, "By weapon", "10m", 8, 1, 15, 0}, 117 | {"NPC", "Serial Killer", 6, ac{12, ""}, atk{8, 1}, "By weapon +3", "10m", 12, 3, 12, 0}, 118 | {"NPC", "Skilled Professional", 1, ac{10, ""}, atk{0, 1}, "By weapon", "10m", 6, 2, 15, 0}, 119 | {"NPC", "Warrior Tyrant", 8, ac{20, "powered"}, atk{10, 1}, "By weapon +3", "10m", 11, 3, 11, 0}, 120 | //Bot", 121 | {"BOT", "Janitor Bot", 1, ac{14, ""}, atk{0, 0}, "N/A", "5m", 8, 1, 15, 1000}, 122 | {"BOT", "Civilian Security Bot", 1, ac{15, ""}, atk{1, 1}, "1d8 stun", "10m", 12, 1, 15, 5000}, 123 | {"BOT", "Repair Bot", 1, ac{14, ""}, atk{0, 1}, "1d6 tool", "10m", 8, 1, 15, 5000}, 124 | {"BOT", "Industrial Work Bot", 2, ac{15, ""}, atk{0, 1}, "1d10 crush", "5m", 8, 1, 14, 2000}, 125 | {"BOT", "Companion Bot", 1, ac{12, ""}, atk{0, 1}, "1d2 unarmed", "10m", 6, 1, 15, 2500}, 126 | {"BOT", "Soldier Bot", 2, ac{16, ""}, atk{1, 1}, "By weapon", "10m", 10, 1, 14, 10000}, 127 | {"BOT", "Heavy Warbot", 6, ac{18, ""}, atk{8, 2}, "2d8 plasma", "15m", 10, 2, 12, 50000}, 128 | // Beasts 129 | {"BEAST", "Small Vicious Beast", 0, ac{14, ""}, atk{1, 1}, "1d2", "10m", 7, 1, 15, 0}, 130 | {"BEAST", "Small Pack Hunter", 1, ac{13, ""}, atk{1, 1}, "1d4", "15m", 8, 1, 15, 0}, 131 | {"BEAST", "Large Pack Hunter", 2, ac{14, ""}, atk{2, 1}, "1d6", "15m", 9, 1, 14, 0}, 132 | {"BEAST", "Large Aggressive Prey Animal", 5, ac{13, ""}, atk{4, 1}, "1d10", "15m", 8, 1, 12, 0}, 133 | {"BEAST", "Lesser Lone Predator", 3, ac{14, ""}, atk{4, 2}, "1d8 each", "15m", 8, 2, 14, 0}, 134 | {"BEAST", "Greater Lone Predator", 5, ac{15, ""}, atk{6, 2}, "1d10 each", "10m", 9, 2, 12, 0}, 135 | {"BEAST", "Terrifying Apex Predator", 8, ac{16, ""}, atk{8, 2}, "1d10 each", "20m", 9, 2, 11, 0}, 136 | {"BEAST", "Gengineered Murder Beast", 10, ac{18, ""}, atk{10, 4}, "1d10 each", "20m", 11, 3, 10, 0}, 137 | } 138 | -------------------------------------------------------------------------------- /content/poi.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/nboughton/go-roll" 9 | "github.com/nboughton/swnt/content/format" 10 | "github.com/nboughton/swnt/content/table" 11 | ) 12 | 13 | // POI Point of Interest 14 | type POI struct { 15 | Point string 16 | Occupied string 17 | Situation string 18 | } 19 | 20 | // NewPOI roll a new point of interest 21 | func NewPOI() POI { 22 | t := poiTable.Tables[rand.Intn(len(poiTable.Tables))] 23 | 24 | return POI{ 25 | Point: t.Name, 26 | Occupied: t.SubTable1.Roll(), 27 | Situation: t.SubTable2.Roll(), 28 | } 29 | } 30 | 31 | // Format returns the POI formatted as type t 32 | func (p POI) Format(t format.OutputType) string { 33 | buf := new(bytes.Buffer) 34 | 35 | fmt.Fprintf(buf, format.Table(t, []string{p.Point, ""}, [][]string{ 36 | {poiTable.Headers[1], p.Occupied}, 37 | {poiTable.Headers[2], p.Situation}, 38 | })) 39 | 40 | return buf.String() 41 | } 42 | 43 | func (p POI) String() string { 44 | return p.Format(format.TEXT) 45 | } 46 | 47 | /*************** TABLES ***************/ 48 | 49 | // Table represents the linked roll tables for Point of Interest generation as described in 50 | // Stars Without Number (Revised Edition) pg 171 51 | var poiTable = table.ThreePart{ 52 | Headers: [3]string{"A Point", "Occupied By", "With This Situation"}, 53 | Tables: []table.ThreePartSubTable{ 54 | { 55 | Name: "Deep-space station", 56 | SubTable1: roll.List{ 57 | Items: []string{ 58 | "Dangerously odd transhumans", 59 | "Freeze-dried ancient corpses", 60 | "Secretive military observers", 61 | "Eccentric oligarch and minions", 62 | "Deranged but brilliant scientist", 63 | }, 64 | }, 65 | SubTable2: roll.List{ 66 | Items: []string{ 67 | "Systems breaking down", 68 | "Foreign sabotage attempt", 69 | "Black market for the elite", 70 | "Vault for dangerous pretech", 71 | "Supply base for pirates", 72 | }, 73 | }, 74 | }, 75 | { 76 | Name: "Asteroid base", 77 | SubTable1: roll.List{ 78 | Items: []string{ 79 | "Zealous religious sectarians", 80 | "Failed rebels from another world", 81 | "Wage-slave corporate miners", 82 | "Independent asteroid prospectors", 83 | "Pirates masquerading as otherwise", 84 | }, 85 | }, 86 | SubTable2: roll.List{ 87 | Items: []string{ 88 | "Life support is threatened", 89 | "Base needs a new asteroid", 90 | "Dug out something nasty", 91 | "Fighting another asteroid", 92 | "Hit a priceless vein of ore", 93 | }, 94 | }, 95 | }, 96 | { 97 | Name: "Remote moon base", 98 | SubTable1: roll.List{ 99 | Items: []string{ 100 | "Unlucky corporate researchers", 101 | "Reclusive hermit genius", 102 | "Remnants of a failed colony", 103 | "Military listening post", 104 | "Lonely overseers and robot miners", 105 | }, 106 | }, 107 | SubTable2: roll.List{ 108 | Items: []string{ 109 | "Something dark has awoken", 110 | "Criminals trying to take over", 111 | "Moon plague breaking out", 112 | "Desperate for vital supplies", 113 | "Rich but badly-protected", 114 | }, 115 | }, 116 | }, 117 | { 118 | Name: "Ancient orbital ruin", 119 | SubTable1: roll.List{ 120 | Items: []string{ 121 | "Robots of dubious sentience", 122 | "Trigger-happy scavengers", 123 | "Government researchers", 124 | "Military quarantine enforcers", 125 | "Heirs of the original alien builders", 126 | }, 127 | }, 128 | SubTable2: roll.List{ 129 | Items: []string{ 130 | "Trying to stop it awakening", 131 | "Meddling with strange tech", 132 | "Impending tech calamity", 133 | "A terrible secret is unearthed", 134 | "Fighting outside interlopers", 135 | }, 136 | }, 137 | }, 138 | { 139 | Name: "Research base", 140 | SubTable1: roll.List{ 141 | Items: []string{ 142 | "Experiments that have gotten loose", 143 | "Scientists from a major local corp", 144 | "Black-ops governmental researchers", 145 | "Secret employees of a foreign power", 146 | "Aliens studying the human locals", 147 | }, 148 | }, 149 | SubTable2: roll.List{ 150 | Items: []string{ 151 | "Perilous research underway", 152 | "Hideously immoral research", 153 | "Held hostage by outsiders", 154 | "Science monsters run amok", 155 | "Selling black-market tech", 156 | }, 157 | }, 158 | }, 159 | { 160 | Name: "Asteroid belt", 161 | SubTable1: roll.List{ 162 | Items: []string{ 163 | "Grizzled belter mine laborers", 164 | "Ancient automated guardian drones", 165 | "Survivors of destroyed asteroid base", 166 | "Pirates hiding out among the rocks", 167 | "Lonely military patrol base staff", 168 | }, 169 | }, 170 | SubTable2: roll.List{ 171 | Items: []string{ 172 | "Ruptured rock released a peril", 173 | "Foreign spy ships hide there", 174 | "Gold rush for new minerals", 175 | "Ancient ruins dot the rocks", 176 | "War between rival rocks", 177 | }, 178 | }, 179 | }, 180 | { 181 | Name: "Gas giant mine", 182 | SubTable1: roll.List{ 183 | Items: []string{ 184 | "Miserable gas-miner slaves or serfs", 185 | "Strange robots and their overseers", 186 | "Scientists studying the alien life", 187 | "Scrappers in the ruined old mine", 188 | "Impoverished separatist group", 189 | }, 190 | }, 191 | SubTable2: roll.List{ 192 | Items: []string{ 193 | "Things are emerging below", 194 | "They need vital supplies", 195 | "The workers are in revolt", 196 | "Pirates secretly fuel there", 197 | "Alien remnants were found", 198 | }, 199 | }, 200 | }, 201 | { 202 | Name: "Refueling station", 203 | SubTable1: roll.List{ 204 | Items: []string{ 205 | "Half-crazed hermit caretaker", 206 | "Sordid purveyors of decadent fun", 207 | "Extortionate corporate minions", 208 | "Religious missionaries to travelers", 209 | "Brainless automated vendors", 210 | }, 211 | }, 212 | SubTable2: roll.List{ 213 | Items: []string{ 214 | "A ship is in severe distress", 215 | "Pirates have taken over", 216 | "Has corrupt customs agents", 217 | "Foreign saboteurs are active", 218 | "Deep-space alien signal", 219 | }, 220 | }, 221 | }, 222 | }, 223 | } 224 | -------------------------------------------------------------------------------- /content/encounter.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "github.com/nboughton/go-roll" 5 | "github.com/nboughton/swnt/content/format" 6 | "github.com/nboughton/swnt/content/table" 7 | ) 8 | 9 | // Encounter represents an encounter 10 | type Encounter struct { 11 | Type string 12 | Fields [][]string 13 | } 14 | 15 | // NewEncounter creates a new encounter 16 | func NewEncounter(wilderness bool) Encounter { 17 | if wilderness { 18 | return Encounter{ 19 | Type: "Wilderness", 20 | Fields: wildernessEncounterTable.Roll(), 21 | } 22 | } 23 | 24 | return Encounter{ 25 | Type: "Urban", 26 | Fields: urbanEncounterTable.Roll(), 27 | } 28 | } 29 | 30 | // Format e as output type t 31 | func (e Encounter) Format(t format.OutputType) string { 32 | return format.Table(t, []string{e.Type + " Encounter", ""}, e.Fields) 33 | } 34 | 35 | func (e Encounter) String() string { 36 | return e.Format(format.TEXT) 37 | } 38 | 39 | // Urban represents the OneRoll tables for rolling quick encounters 40 | var urbanEncounterTable = table.OneRoll{ 41 | D4: roll.List{ 42 | Name: "What's the Conflict About?", 43 | Items: []string{ 44 | "Money, extortion, payment due, debts", 45 | "Respect, submission to social authority", 46 | "Grudges, ethnic resentment, gang payback", 47 | "Politics, religion, or other ideology", 48 | }, 49 | }, 50 | D6: roll.List{ 51 | Name: "General Venue of the Event", 52 | Items: []string{ 53 | "In the middle of the street", 54 | "In a public plaza", 55 | "Down a side alley", 56 | "Inside a local business", 57 | "Next to or in a public park", 58 | "At a mass-transit station", 59 | }, 60 | }, 61 | D8: roll.List{ 62 | Name: "Why are the PCs Involved?", 63 | Items: []string{ 64 | "A sympathetic participant appeals to them", 65 | "Ways around it are all dangerous/blocked", 66 | "It happens immediately around them", 67 | "A valuable thing looks snatchable amid it", 68 | "A participant offers a reward for help", 69 | "Someone mistakenly involves the PCs in it", 70 | "The seeming way out just leads deeper in", 71 | "Responsibility is somehow pinned on them", 72 | }, 73 | }, 74 | D10: roll.List{ 75 | Name: "What's the Nature of the Event?", 76 | Items: []string{ 77 | "A parade or festival is being disrupted", 78 | "Innocents are being assaulted", 79 | "An establishment is being robbed", 80 | "A disturbance over local politics happens", 81 | "Someone is being blamed for something", 82 | "Fires or building collapses are happening", 83 | "A medical emergency is happening", 84 | "Someone’s trying to cheat the PCs", 85 | "A vehicle accident is happening", 86 | "A religious ceremony is being disrupted", 87 | }, 88 | }, 89 | D12: roll.List{ 90 | Name: "What Antagonists are Involved?", 91 | Items: []string{ 92 | "A local bully and their thugs", 93 | "A ruthless political boss and their zealots", 94 | "Violent criminals", 95 | "Religious fanatics", 96 | "A blisteringly obnoxious offworlder", 97 | "Corrupt or over-strict government official", 98 | "A mob of intoxicated locals", 99 | "A ranting demagogue and their followers", 100 | "A stupidly bull-headed local grandee", 101 | "A very capable assassin or strong-arm", 102 | "A self-centered local scion of power", 103 | "A confused foreigner or backwoodsman", 104 | }, 105 | }, 106 | D20: roll.List{ 107 | Name: "Relevant Urban Features", 108 | Items: []string{ 109 | "Heavy traffic running through the place", 110 | "Music blaring at deafening volumes", 111 | "Two groups present that detest each other", 112 | "Large delivery taking place right there", 113 | "Swarm of schoolkids or feral youth", 114 | "Insistent soapbox preacher here", 115 | "Several pickpockets working the crowd", 116 | "A kiosk is tipping over and spilling things", 117 | "Streetlights are out or visibility is low", 118 | "A cop patrol is here and reluctant to act", 119 | "PC-hostile reporters are recording here", 120 | "Someone’s trying to sell something to PCs", 121 | "Feral dogs or other animals crowd here", 122 | "Unrelated activists are protesting here", 123 | "Street kids are trying to steal from the PCs", 124 | "GPS maps are dangerously wrong here", 125 | "Downed power lines are a danger here", 126 | "Numerous open manholes and utility holes", 127 | "The street’s blockaded by something", 128 | "Crowds so thick one can barely move", 129 | }, 130 | }, 131 | } 132 | 133 | // Wilderness represents the OneRoll tables for generating Wilderness Encounters 134 | var wildernessEncounterTable = table.OneRoll{ 135 | D4: roll.List{ 136 | Name: "Initial Encounter Range", 137 | Items: []string{ 138 | "Visible from a long distance away", 139 | "Noticed 1d4 hundred meters away", 140 | "Noticed only within 1d6 x 10 meters", 141 | "Noticed only when adjacent to the event", 142 | }, 143 | }, 144 | D6: roll.List{ 145 | Name: "Weather and Lighting", 146 | Items: []string{ 147 | "Takes place in daylight and clear weather", 148 | "Daylight, but fog, mist, rain or the like", 149 | "Daylight, but harsh seasonal weather", 150 | "Night encounter, but clear weather", 151 | "Night, with rain or other obscuring effects", 152 | "Night, with terrible weather and wind", 153 | }, 154 | }, 155 | D8: roll.List{ 156 | Name: "Basic Nature of the Encounter", 157 | Items: []string{ 158 | "Attack by pack of hostiles", 159 | "Ambush by single lone hostile", 160 | "Meet people who don’t want to be met", 161 | "Encounter people in need of aid", 162 | "Encounter hostile creatures", 163 | "Nearby feature is somehow dangerous", 164 | "Nearby feature promises useful loot", 165 | "Meet hostiles that aren’t immediately so", 166 | }, 167 | }, 168 | D10: roll.List{ 169 | Name: "Types of Friendly Creatures", 170 | Items: []string{ 171 | "Affable but reclusive hermit", 172 | "Local herd animal let loose to graze", 173 | "Government ranger or circuit judge", 174 | "Curious local animal", 175 | "Remote homesteader and family", 176 | "Working trapper or hunter", 177 | "Back-country villager or native", 178 | "Hiker or wilderness tourist", 179 | "Religious recluse or holy person", 180 | "Impoverished social exile", 181 | }, 182 | }, 183 | D12: roll.List{ 184 | Name: "Types of Hostile Creatures", 185 | Items: []string{ 186 | "Bandits in their wilderness hideout", 187 | "Dangerous locals looking for easy marks", 188 | "Rabid or diseased large predator", 189 | "Pack of hungry hunting beasts", 190 | "Herd of potentially dangerous prey animals", 191 | "Swarm of dangerous vermin", 192 | "Criminal seeking to evade the law", 193 | "Brutal local landowner and their men", 194 | "Crazed hermit seeking enforced solitude", 195 | "Friendly-seeming guide into lethal danger", 196 | "Harmless-looking but dangerous beast", 197 | "Confidence man seeking to gull the PCs", 198 | }, 199 | }, 200 | D20: roll.List{ 201 | Name: "Specific Nearby Feature of Relevance", 202 | Items: []string{ 203 | "Overgrown homestead", 204 | "Stream prone to flash-flooding", 205 | "Narrow bridge or beam over deep cleft", 206 | "Box canyon with steep sides", 207 | "Unstable hillside that slides if disturbed", 208 | "Long-lost crash site of a gravflyer", 209 | "Once-inhabited cave or tunnel", 210 | "Steep and dangerous cliff", 211 | "Quicksand-laden swamp or dust pit", 212 | "Ruins of a ghost town or lost hamlet", 213 | "Hunting cabin with necessities", 214 | "Ill-tended graveyard of a lost family stead", 215 | "Narrow pass that’s easily blocked", 216 | "Dilapidated resort building", 217 | "Remote government monitoring outpost", 218 | "Illicit substance farm or processing center", 219 | "Old and forgotten battleground", 220 | "Zone overrun by dangerous plants", 221 | "Thick growth that lights up at a spark", 222 | "Abandoned vehicle", 223 | }, 224 | }, 225 | } 226 | -------------------------------------------------------------------------------- /content/conflict.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/nboughton/go-roll" 8 | "github.com/nboughton/swnt/content/format" 9 | "github.com/nboughton/swnt/content/table" 10 | ) 11 | 12 | // Conflict is pretty self explanatory 13 | type Conflict struct { 14 | Restraint string 15 | Twist string 16 | Problem [][]string 17 | } 18 | 19 | // NewConflict for fun and profit 20 | func NewConflict() Conflict { 21 | return Conflict{ 22 | Restraint: conflictTable.restraint.Roll(), 23 | Twist: conflictTable.twist.Roll(), 24 | Problem: conflictTable.problem.Roll(), 25 | } 26 | } 27 | 28 | // Format conflict c as type t 29 | func (c Conflict) Format(t format.OutputType) string { 30 | buf := new(bytes.Buffer) 31 | 32 | fmt.Fprintf(buf, format.Table(t, []string{"Conflict", ""}, c.Problem)) 33 | fmt.Fprintf(buf, format.Table(t, []string{}, [][]string{ 34 | {"Twist", c.Twist}, 35 | {"Restraint", c.Restraint}, 36 | })) 37 | 38 | return buf.String() 39 | } 40 | 41 | func (c Conflict) string() string { 42 | return c.Format(format.TEXT) 43 | } 44 | 45 | var conflictTable = struct { 46 | restraint roll.List 47 | twist roll.List 48 | problem table.ThreePart 49 | }{ 50 | // Restraint represents possible conflict restraints 51 | roll.List{ 52 | Items: []string{ 53 | "The government is cracking down on the conflict", 54 | "One side seems invincibly stronger to the other", 55 | "Both sides have “doomsday” info or devices", 56 | "A prior conflict ended horribly for both of them", 57 | "Foreign participants are keeping things tamped", 58 | "Elements of both sides seek accommodation", 59 | "The conflict is only viable in a narrow location", 60 | "Catastrophic cost of losing a direct showdown", 61 | "Each thinks they’ll win without further exertion", 62 | "They expect a better opening to appear soon", 63 | "Former ties of friendship or family restrain them", 64 | "Religious principles are constraining them", 65 | "One side’s still licking their wounds after a failure", 66 | "They’re building up force to make sure they win", 67 | "Their cultural context makes open struggle hard", 68 | "They expect an outside power to hand them a win", 69 | "They’re still searching for a way to get at their goal", 70 | "One side mistakenly thinks they’ve already won", 71 | "A side is busy integrating a recent success", 72 | "An outside power threatens both sides", 73 | }, 74 | }, 75 | 76 | // Twist represents possible adventure twists 77 | roll.List{ 78 | Items: []string{ 79 | "There’s a very sharp time limit for any resolution", 80 | "The sympathetic side is actually a bunch of bastards", 81 | "There’s an easy but very repugnant solution to hand", 82 | "PC success means a big benefit to a hostile group", 83 | "The real bone of contention is hidden from most", 84 | "A sympathetic figure’s on an unsympathetic side", 85 | "There’s a profitable chance for PCs to turn traitor", 86 | "The “winner” will actually get in terrible trouble", 87 | "There’s a very appealing third party in the mix", 88 | "The PCs could really profit off the focus of the strife", 89 | "The PCs are mistaken for an involved group", 90 | "Somebody plans on screwing over the PCs", 91 | "Both sides think the PCs are working for them", 92 | "A side wants to use the PCs as a distraction for foes", 93 | "The PCs’ main contact is mistrusted by their allies", 94 | "If the other side can’t get it, they’ll destroy it", 95 | "The focus isn’t nearly as valuable as both sides think", 96 | "The focus somehow has its own will and goals", 97 | "Victory will drastically change one of the sides", 98 | "Actually, there is no twist. It’s all exactly as it seems.", 99 | }, 100 | }, 101 | 102 | // Problem represents a problem the party might need to solve 103 | table.ThreePart{ 104 | Headers: [3]string{"Conflict Type", "Overall Situation", "Specific Focus"}, 105 | Tables: []table.ThreePartSubTable{ 106 | { 107 | Name: "Money", 108 | SubTable1: roll.List{ 109 | Items: []string{ 110 | "Money is owed to a ruthless creditor", 111 | "Money was stolen from someone", 112 | "A sudden profit opportunity arises", 113 | "There’s a hidden stash of wealth", 114 | "Money is offered from an evil source", 115 | }, 116 | }, 117 | SubTable2: roll.List{ 118 | Items: []string{ 119 | "Organized crime wants it", 120 | "Corrupt officials want it", 121 | "A sympathetic NPC needs it", 122 | "The PCs are owed it", 123 | "It will disappear very soon", 124 | }, 125 | }, 126 | }, 127 | { 128 | Name: "Revenge", 129 | SubTable1: roll.List{ 130 | Items: []string{ 131 | "Someone was murdered", 132 | "Someone was stripped of rank", 133 | "Someone lost all their wealth", 134 | "Someone lost someone’s love", 135 | "Someone was framed for a crime", 136 | }, 137 | }, 138 | SubTable2: roll.List{ 139 | Items: []string{ 140 | "It was wholly justified", 141 | "The wrong person is targeted", 142 | "The reaction is excessive", 143 | "The PCs are somehow blamed", 144 | "Both sides were wronged", 145 | }, 146 | }, 147 | }, 148 | { 149 | Name: "Power", 150 | SubTable1: roll.List{ 151 | Items: []string{ 152 | "An influential political leader", 153 | "A stern community elder", 154 | "A ruling patriarch of a large family", 155 | "A star expert in a particular industry", 156 | "A criminal boss or outcast leader", 157 | }, 158 | }, 159 | SubTable2: roll.List{ 160 | Items: []string{ 161 | "They’ve betrayed their own", 162 | "Someone’s gunning for them", 163 | "They made a terrible choice", 164 | "They usurped their position", 165 | "They’re oppressing their own", 166 | }, 167 | }, 168 | }, 169 | { 170 | Name: "Natural Danger", 171 | SubTable1: roll.List{ 172 | Items: []string{ 173 | "A cyclical planetary phenomenon", 174 | "A sudden natural disaster", 175 | "Sudden loss of vital infrastructure", 176 | "Catastrophe from outside meddling", 177 | "Formerly-unknown planetary peril", 178 | }, 179 | }, 180 | SubTable2: roll.List{ 181 | Items: []string{ 182 | "Anti-helpful bureaucrats", 183 | "Religious zealots panic", 184 | "Bandits and looters strike", 185 | "The government hushes it up", 186 | "There’s money in exploiting it", 187 | }, 188 | }, 189 | }, 190 | { 191 | Name: "Religion", 192 | SubTable1: roll.List{ 193 | Items: []string{ 194 | "Sects that hate each other bitterly", 195 | "Zealot reformers forcing new things", 196 | "Radical traditionalists fighting back", 197 | "Ethnic religious divisions", 198 | "Corrupt and decadent institutions", 199 | }, 200 | }, 201 | SubTable2: roll.List{ 202 | Items: []string{ 203 | "Charismatic new leader", 204 | "Mandatory state religion", 205 | "Heavy foreign influence", 206 | "Religious purging underway", 207 | "Fighting for holy ground", 208 | }, 209 | }, 210 | }, 211 | { 212 | Name: "Ideology", 213 | SubTable1: roll.List{ 214 | Items: []string{ 215 | "A universally-despised fringe group", 216 | "Terrorists with widespread support", 217 | "A political party’s goon squads", 218 | "Dead-end former regime supporters", 219 | "Ruthless ascendant political group", 220 | }, 221 | }, 222 | SubTable2: roll.List{ 223 | Items: []string{ 224 | "Terrorist attack", 225 | "Street rioting", 226 | "Police state crackdown", 227 | "Forced expulsions", 228 | "Territory under hostile rule", 229 | }, 230 | }, 231 | }, 232 | { 233 | Name: "Ethnicity", 234 | SubTable1: roll.List{ 235 | Items: []string{ 236 | "A traditionally subordinate group", 237 | "An ethnic group from offworld", 238 | "A dominant caste or ethnicity", 239 | "An alien or transhuman group", 240 | "Two groups that hate each other", 241 | }, 242 | }, 243 | SubTable2: roll.List{ 244 | Items: []string{ 245 | "Forced immigration", 246 | "Official ethnic ghettos", 247 | "Rigid separation of groups", 248 | "Group statuses have changed", 249 | "Rising ethnic violence", 250 | }, 251 | }, 252 | }, 253 | { 254 | Name: "Resources", 255 | SubTable1: roll.List{ 256 | Items: []string{ 257 | "There’s a cache of illegal materials", 258 | "A hidden strike of rare resources", 259 | "Cargo has been abandoned as lost", 260 | "Land ownership is disputed", 261 | "A resource is desperately necessary", 262 | }, 263 | }, 264 | SubTable2: roll.List{ 265 | Items: []string{ 266 | "Someone thinks they own it", 267 | "The state is looking for it", 268 | "It has its own protectors", 269 | "Rights to it were stolen", 270 | "Offworlders want it badly", 271 | }, 272 | }, 273 | }, 274 | }, 275 | }, 276 | } 277 | -------------------------------------------------------------------------------- /content/place.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/nboughton/go-roll" 8 | "github.com/nboughton/swnt/content/format" 9 | "github.com/nboughton/swnt/content/table" 10 | ) 11 | 12 | // Place represents the aggregate details of a generated place 13 | type Place struct { 14 | Reward string 15 | Ongoings string 16 | Hazard [][]string 17 | } 18 | 19 | // NewPlace roll a new place, default to urban 20 | func NewPlace(wilderness bool) Place { 21 | og := "" 22 | if wilderness { 23 | og = placeTable.ongoingsWild.Roll() 24 | } else { 25 | og = placeTable.ongoingsCiv.Roll() 26 | } 27 | 28 | return Place{ 29 | Reward: placeTable.reward.Roll(), 30 | Ongoings: og, 31 | Hazard: placeTable.hazard.Roll(), 32 | } 33 | } 34 | 35 | // Format returns Place formatted as type t 36 | func (p Place) Format(t format.OutputType) string { 37 | buf := new(bytes.Buffer) 38 | 39 | fmt.Fprintf(buf, format.Table(t, []string{"Place", ""}, p.Hazard)) 40 | fmt.Fprintf(buf, format.Table(t, []string{}, [][]string{ 41 | {"Ongoings", p.Ongoings}, 42 | {"Reward", p.Reward}, 43 | })) 44 | 45 | return buf.String() 46 | } 47 | 48 | func (p Place) String() string { 49 | return p.Format(format.TEXT) 50 | } 51 | 52 | var placeTable = struct { 53 | reward roll.List 54 | ongoingsCiv roll.List 55 | ongoingsWild roll.List 56 | hazard table.ThreePart 57 | }{ 58 | // Reward roll reward 59 | roll.List{ 60 | Items: []string{ 61 | "Large cache of credits", 62 | "Precious cultural artifact", 63 | "Vital data on the party’s goal", 64 | "Missing or kidnapped VIP", 65 | "Advanced pretech artifact", 66 | "Key to some guarded location", 67 | "Ancient treasure object", 68 | "Recently-stolen goods", 69 | "High-tech robotic servitor", 70 | "Token item of ruling legitimacy", 71 | "Juicy blackmail material", 72 | "History-rewriting evidence", 73 | "Alien artifact of great power", 74 | "Precious megacorp data files", 75 | "Map to some valuable thing", 76 | "Forbidden but precious drug", 77 | "Legal title to important land", 78 | "Awful secret of local government", 79 | "Cache of precious goods", 80 | "Stock of valuable weaponry", 81 | }, 82 | }, 83 | 84 | // Ongoings roll ongoings 85 | // civ 86 | roll.List{ 87 | Items: []string{ 88 | "Local festival going on", 89 | "Angry street protests", 90 | "Minor fire or other disorder", 91 | "Merchants and peddlers active", 92 | "Tourists from another country", 93 | "Building repair or maintenance", 94 | "Recent vehicle crash", 95 | "Public art performance", 96 | "Angry traffic jam", 97 | "Missionaries for a local religion", 98 | "Loud advertising campaign", 99 | "Memorial service ongoing", 100 | "Road work halting traffic", 101 | "Power outage in the area", 102 | "Police chasing criminals", 103 | "Annoying drunks being loud", 104 | "Beggars seeking alms", 105 | "Constructing a new building", 106 | "Local thugs swaggering around", 107 | "Aerial light display", 108 | }, 109 | }, 110 | 111 | // wild 112 | roll.List{ 113 | Items: []string{ 114 | "Bandits have moved in", 115 | "Flooding swept through", 116 | "Part of it has collapsed", 117 | "Refugees are hiding here", 118 | "Dangerous animals lair here", 119 | "A rebel cell uses it for a base", 120 | "Smugglers have landed here", 121 | "Foreign agents meet here", 122 | "A hermit has taken up residence", 123 | "A toxic plant is growing wild", 124 | "An artist seeks inspiration here", 125 | "An ancient structure was dug out", 126 | "The weather has turned savage", 127 | "A vehicle crashed nearby", 128 | "Some locals are badly lost", 129 | "Religious pilgrims come here", 130 | "Locals fight over control of it", 131 | "Nature threatens to wipe it out", 132 | "An old shrine was raised here", 133 | "A shell of a building remains", 134 | }, 135 | }, 136 | 137 | // Hazard roll hazards 138 | table.ThreePart{ 139 | Headers: [3]string{"Hazard", "Specific Example", "Possible Danger"}, 140 | Tables: []table.ThreePartSubTable{ 141 | { 142 | Name: "Social", 143 | SubTable1: roll.List{ 144 | Items: []string{ 145 | "An explosively temperamental VIP", 146 | "An unknown but critical social taboo", 147 | "A case of mistaken identity", 148 | "An expectation for specific PC action", 149 | "A frame job hung on the PCs", 150 | }, 151 | }, 152 | SubTable2: roll.List{ 153 | Items: []string{ 154 | "An allied NPC breaks ties", 155 | "An enemy is alerted to them", 156 | "A new enemy is made", 157 | "Cads think the PCs are allies", 158 | "An opportunity is lost", 159 | }, 160 | }, 161 | }, 162 | { 163 | Name: "Legal", 164 | SubTable1: roll.List{ 165 | Items: []string{ 166 | "A regulation unknown to the PCs", 167 | "A tax or confiscation", 168 | "Vital gear is prohibited here", 169 | "Lawsuit from an aggrieved NPC", 170 | "A state agent conscripts PC help", 171 | }, 172 | }, 173 | SubTable2: roll.List{ 174 | Items: []string{ 175 | "Substantial monetary fine", 176 | "Imprisonment for the party", 177 | "Confiscation of possessions", 178 | "Deportation from the place", 179 | "Loss of rights and protections", 180 | }, 181 | }, 182 | }, 183 | { 184 | Name: "Environmental", 185 | SubTable1: roll.List{ 186 | Items: []string{ 187 | "Heavy background radiation", 188 | "A planetary sickness foreigners get", 189 | "Strong or weak local gravity", 190 | "Gear-eating microbial life", 191 | "Unpredictable psychic power field", 192 | }, 193 | }, 194 | SubTable2: roll.List{ 195 | Items: []string{ 196 | "Catch a lingering disease", 197 | "Suffer bodily harm", 198 | "Take a penalty on rolls", 199 | "Lose some equipment", 200 | "Psychic abilities are altered", 201 | }, 202 | }, 203 | }, 204 | { 205 | Name: "Trap", 206 | SubTable1: roll.List{ 207 | Items: []string{ 208 | "Alarm system attached to a trap", 209 | "Snare left for local animals", 210 | "Hermit’s self-defense measure", 211 | "Long-dead builder’s trapsmithing", 212 | "New occupant’s defensive trap", 213 | }, 214 | }, 215 | SubTable2: roll.List{ 216 | Items: []string{ 217 | "Something set on fire", 218 | "Guards are summoned", 219 | "Fall to a new area", 220 | "Equipment is damaged", 221 | "Subject is injured", 222 | }, 223 | }, 224 | }, 225 | { 226 | Name: "Animal", 227 | SubTable1: roll.List{ 228 | Items: []string{ 229 | "Dangerous local swarm vermin", 230 | "A big predator lair", 231 | "Pack hunters haunt the area", 232 | "Flying threats pounce here", 233 | "Monstrous beast sleeps or is torpid", 234 | }, 235 | }, 236 | SubTable2: roll.List{ 237 | Items: []string{ 238 | "They have a ranged attack", 239 | "They’re venomous", 240 | "Dangerously coordinated foe", 241 | "Killing them inflicts a fine", 242 | "Their deaths cause an effect", 243 | }, 244 | }, 245 | }, 246 | { 247 | Name: "Sentient", 248 | SubTable1: roll.List{ 249 | Items: []string{ 250 | "A group hostile to intruders", 251 | "Trickster thieves and con-men", 252 | "Hostile expert-system robots", 253 | "Secrecy-loving rebels or criminals", 254 | "Another area-clearing group", 255 | }, 256 | }, 257 | SubTable2: roll.List{ 258 | Items: []string{ 259 | "Immediate combat", 260 | "Treacherous feigned friend", 261 | "Lead the PCs into a trap", 262 | "Demand payment or loot", 263 | "Activate other area defenses", 264 | }, 265 | }, 266 | }, 267 | { 268 | Name: "Decay", 269 | SubTable1: roll.List{ 270 | Items: []string{ 271 | "Crumbling floor or ceiling", 272 | "Waste or heating tubes rupture", 273 | "Dangerous standing liquid", 274 | "Maintenance robots gone haywire", 275 | "Power plant is unstable", 276 | }, 277 | }, 278 | SubTable2: roll.List{ 279 | Items: []string{ 280 | "Ruptures to release a peril", 281 | "Toxic or radioactive debris", 282 | "Explosive decompression", 283 | "Invisible or slow-acting toxin", 284 | "Fires or explosions", 285 | }, 286 | }, 287 | }, 288 | { 289 | Name: "PC-induced", 290 | SubTable1: roll.List{ 291 | Items: []string{ 292 | "Activating a system causes a disaster", 293 | "Catastrophic plan proposed by NPCs", 294 | "Removing loot triggers defenses", 295 | "Handling an object ruins it", 296 | "Leaving a thing open brings calamity", 297 | }, 298 | }, 299 | SubTable2: roll.List{ 300 | Items: []string{ 301 | "Horrible vermin are admitted", 302 | "Local system goes berserk", 303 | "Something ruptures violently", 304 | "Ancient defenses awaken", 305 | "The PC’s goal is imperiled", 306 | }, 307 | }, 308 | }, 309 | }, 310 | }, 311 | } 312 | -------------------------------------------------------------------------------- /content/beast.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | 9 | "github.com/nboughton/go-roll" 10 | "github.com/nboughton/swnt/content/format" 11 | "github.com/nboughton/swnt/content/table" 12 | "github.com/nboughton/swnt/dice" 13 | ) 14 | 15 | func init() { 16 | table.Registry.Add(beastFeaturesTable.basicFeatures) 17 | table.Registry.Add(beastFeaturesTable.bodyPlan) 18 | } 19 | 20 | // Beast defines the aggregate descriptors for an animal 21 | type Beast struct { 22 | Type string 23 | Behaviour string 24 | Features string 25 | BodyPlan string 26 | LimbNovelty string 27 | SkinNovelty string 28 | MainWeapon string 29 | Size string 30 | } 31 | 32 | // NewBeast for terrorising players 33 | func NewBeast() Beast { 34 | b := Beast{ 35 | Features: beastFeaturesTable.basicFeatures.Roll(), 36 | BodyPlan: beastFeaturesTable.bodyPlan.Roll(), 37 | LimbNovelty: beastFeaturesTable.limbNovelty.Roll(), 38 | SkinNovelty: beastFeaturesTable.skinNovelty.Roll(), 39 | MainWeapon: beastFeaturesTable.mainWeapon.Roll(), 40 | Size: beastFeaturesTable.size.Roll(), 41 | } 42 | 43 | switch rand.Intn(3) { 44 | case 0: 45 | b.Type = beastBehaviourTable.predator.Label() 46 | b.Behaviour = beastBehaviourTable.predator.Roll() 47 | 48 | case 1: 49 | b.Type = beastBehaviourTable.prey.Label() 50 | b.Behaviour = beastBehaviourTable.prey.Roll() 51 | 52 | case 2: 53 | b.Type = beastBehaviourTable.scavenger.Label() 54 | b.Behaviour = beastBehaviourTable.scavenger.Roll() 55 | } 56 | 57 | return b 58 | } 59 | 60 | // Format output as format type t 61 | func (b Beast) Format(t format.OutputType) string { 62 | buf := new(bytes.Buffer) 63 | 64 | fmt.Fprintf(buf, format.Table(t, []string{"Beast", ""}, [][]string{ 65 | {b.Type, b.Behaviour}, 66 | {beastFeaturesTable.basicFeatures.Label(), b.Features}, 67 | {beastFeaturesTable.bodyPlan.Label(), b.BodyPlan}, 68 | {beastFeaturesTable.limbNovelty.Label(), b.LimbNovelty}, 69 | {beastFeaturesTable.skinNovelty.Label(), b.SkinNovelty}, 70 | {beastFeaturesTable.mainWeapon.Label(), b.MainWeapon}, 71 | {beastFeaturesTable.size.Label(), b.Size}, 72 | })) 73 | 74 | return buf.String() 75 | } 76 | 77 | func (b Beast) String() string { 78 | return b.Format(format.TEXT) 79 | } 80 | 81 | var beastFeaturesTable = struct { 82 | basicFeatures roll.Table 83 | bodyPlan roll.Table 84 | limbNovelty roll.Table 85 | skinNovelty roll.Table 86 | mainWeapon roll.Table 87 | size roll.Table 88 | }{ 89 | // BasicAnimalFeatures is pretty fucking self-explanatory 90 | roll.Table{ 91 | Name: "Basic Features", 92 | ID: "beast.BasicFeatures", 93 | Dice: roll.Dice{N: 1, Die: roll.D10}, 94 | Items: []roll.TableItem{ 95 | {Match: []int{1}, Text: "Amphibian, froggish or newtlike"}, 96 | {Match: []int{2}, Text: "Bird, winged and feathered"}, 97 | {Match: []int{3}, Text: "Fish, scaled and torpedo-bodied"}, 98 | {Match: []int{4}, Text: "Insect, beetle-like or fly-winged"}, 99 | {Match: []int{5}, Text: "Mammal, hairy and fanged"}, 100 | {Match: []int{6}, Text: "Reptile, lizardlike and long-bodied"}, 101 | {Match: []int{7}, Text: "Spider, many-legged and fat"}, 102 | {Match: []int{8}, Text: "Exotic, made of wholly alien elements"}, 103 | {Match: []int{9, 10}, Text: "Mixed", Action: func() string { 104 | tbl, _ := table.Registry.Get("beast.BasicFeatures") 105 | tbl.Dice = roll.Dice{N: 1, Die: roll.D8} 106 | 107 | res := make(map[string]bool) 108 | for len(res) < 2 { 109 | res[tbl.Roll()] = true 110 | } 111 | 112 | text := []string{} 113 | for k := range res { 114 | text = append(text, k) 115 | } 116 | 117 | return strings.Join(text, " and ") 118 | }}, 119 | }, 120 | }, 121 | 122 | // BodyPlan p201 SWN:RE Free edition 123 | roll.Table{ 124 | Name: "Body Plan", 125 | ID: "beast.BodyPlan", 126 | Dice: roll.Dice{N: 1, Die: roll.D6}, 127 | Items: []roll.TableItem{ 128 | {Match: []int{1}, Text: "Humanoid"}, 129 | {Match: []int{2}, Text: "Quadruped"}, 130 | {Match: []int{3}, Text: "Many-legged"}, 131 | {Match: []int{4}, Text: "Bulbous"}, 132 | {Match: []int{5}, Text: "Amorphous"}, 133 | {Match: []int{6}, Text: "", Action: func() string { 134 | tbl, _ := table.Registry.Get("beast.BodyPlan") 135 | tbl.Dice = roll.Dice{N: 1, Die: dice.D5} 136 | 137 | types, res := 2, make(map[string]bool) 138 | for len(res) < types { 139 | res[tbl.Roll()] = true 140 | } 141 | 142 | text := []string{} 143 | for k := range res { 144 | text = append(text, k) 145 | } 146 | 147 | return strings.Join(text, " and ") 148 | }}, 149 | }, 150 | }, 151 | 152 | // LimbNovelty p201 SWN:RE Free edition 153 | roll.Table{ 154 | Name: "Limb Novelty", 155 | ID: "beast.LimbNovelty", 156 | Dice: roll.Dice{N: 1, Die: roll.D6}, 157 | Items: []roll.TableItem{ 158 | {Match: []int{1}, Text: "Wings"}, 159 | {Match: []int{2}, Text: "Many joints"}, 160 | {Match: []int{3}, Text: "Tentacles"}, 161 | {Match: []int{4}, Text: "Opposable thumbs"}, 162 | {Match: []int{5}, Text: "Retractable"}, 163 | {Match: []int{6}, Text: "Varying sizes"}, 164 | }, 165 | }, 166 | 167 | // SkinNovelty p201 SWN:RE Free edition 168 | roll.Table{ 169 | Name: "Skin Novelty", 170 | ID: "beast.SkinNovelty", 171 | Dice: roll.Dice{N: 1, Die: roll.D6}, 172 | Items: []roll.TableItem{ 173 | {Match: []int{1}, Text: "Hard shell"}, 174 | {Match: []int{2}, Text: "Exoskeleton"}, 175 | {Match: []int{3}, Text: "Odd texture"}, 176 | {Match: []int{4}, Text: "Molts regularly"}, 177 | {Match: []int{5}, Text: "Harmful to touch"}, 178 | {Match: []int{6}, Text: "Wet or slimy"}, 179 | }, 180 | }, 181 | 182 | // MainWeapon p201 SWN:RE Free edition 183 | roll.Table{ 184 | Name: "Main Weapon", 185 | ID: "beast.MainWeapon", 186 | Dice: roll.Dice{N: 1, Die: roll.D6}, 187 | Items: []roll.TableItem{ 188 | {Match: []int{1}, Text: "Teeth or mandibles"}, 189 | {Match: []int{2}, Text: "Claws"}, 190 | {Match: []int{3}, Text: "Poison", Action: poisonAction}, 191 | {Match: []int{4}, Text: "Harmful discharge", Action: func() string { 192 | return harmfulDischargesTable.Roll() 193 | }}, 194 | {Match: []int{5}, Text: "Pincers"}, 195 | {Match: []int{6}, Text: "Horns"}, 196 | }, 197 | }, 198 | 199 | // Size p201 SWN:RE Free edition 200 | roll.Table{ 201 | Name: "Size", 202 | ID: "beast.Size", 203 | Dice: roll.Dice{N: 1, Die: roll.D6}, 204 | Items: []roll.TableItem{ 205 | {Match: []int{1}, Text: "Cat-sized"}, 206 | {Match: []int{2}, Text: "Wolf-sized"}, 207 | {Match: []int{3}, Text: "Calf-sized"}, 208 | {Match: []int{4}, Text: "Bull-sized"}, 209 | {Match: []int{5}, Text: "Hippo-sized"}, 210 | {Match: []int{6}, Text: "Elephant-sized"}, 211 | }, 212 | }, 213 | } 214 | 215 | // Behavioural traits 216 | 217 | // Predator p201 SWN:RE Free edition 218 | var beastBehaviourTable = struct { 219 | predator roll.Table 220 | prey roll.Table 221 | scavenger roll.Table 222 | }{ 223 | roll.Table{ 224 | Name: "Predator", 225 | ID: "beast.Predator", 226 | Dice: roll.Dice{N: 1, Die: roll.D8}, 227 | Items: []roll.TableItem{ 228 | {Match: []int{1}, Text: "Hunts in kin-group packs"}, 229 | {Match: []int{2}, Text: "Favors ambush attacks"}, 230 | {Match: []int{3}, Text: "Cripples prey and waits for death"}, 231 | {Match: []int{4}, Text: "Pack supports alpha-beast attack"}, 232 | {Match: []int{5}, Text: "Lures or drives prey into danger"}, 233 | {Match: []int{6}, Text: "Hunts as a lone, powerful hunter"}, 234 | {Match: []int{7}, Text: "Only is predator at certain times"}, 235 | {Match: []int{8}, Text: "Mindlessly attacks humans"}, 236 | }, 237 | }, 238 | 239 | // Prey p201 SWN:RE Free edition 240 | roll.Table{ 241 | Name: "Prey", 242 | ID: "beast.Prey", 243 | Dice: roll.Dice{N: 1, Die: roll.D8}, 244 | Items: []roll.TableItem{ 245 | {Match: []int{1}, Text: "Moves in vigilant herds"}, 246 | {Match: []int{2}, Text: "Exists in small family groups"}, 247 | {Match: []int{3}, Text: "They all team up on a single foe"}, 248 | {Match: []int{4}, Text: "They go berserk when near death"}, 249 | {Match: []int{5}, Text: "They’re violent in certain seasons"}, 250 | {Match: []int{6}, Text: "They’re vicious if threatened"}, 251 | {Match: []int{7}, Text: "Symbiotic creature protects them"}, 252 | {Match: []int{8}, Text: "Breeds at tremendous rates"}, 253 | }, 254 | }, 255 | 256 | // Scavenger p201 SWN:RE Free edition 257 | roll.Table{ 258 | Name: "Scavenger", 259 | ID: "beast.Scavenger", 260 | Dice: roll.Dice{N: 1, Die: roll.D8}, 261 | Items: []roll.TableItem{ 262 | {Match: []int{1}, Text: "Never attacks unwounded prey"}, 263 | {Match: []int{2}, Text: "Uses other beasts as harriers"}, 264 | {Match: []int{3}, Text: "Always flees if significantly hurt"}, 265 | {Match: []int{4}, Text: "Poisons prey, waits for it to die"}, 266 | {Match: []int{5}, Text: "Disguises itself as its prey"}, 267 | {Match: []int{6}, Text: "Remarkably stealthy"}, 268 | {Match: []int{7}, Text: "Summons predators to weak prey"}, 269 | {Match: []int{8}, Text: "Steals prey from weaker predator"}, 270 | }, 271 | }, 272 | } 273 | 274 | // HarmfulDischarges p201 SWN:RE Free edition 275 | var harmfulDischargesTable = roll.Table{ 276 | Name: "HarmfulDischarges", 277 | ID: "beast.HarmfulDischarges", 278 | Dice: roll.Dice{N: 1, Die: roll.D8}, 279 | Items: []roll.TableItem{ 280 | {Match: []int{1}, Text: "Acidic spew doing its damage on a hit"}, 281 | {Match: []int{2}, Text: "Toxic spittle or cloud; ", Action: poisonAction}, 282 | {Match: []int{3}, Text: "Super-heated or super-chilled spew"}, 283 | {Match: []int{4}, Text: "Sonic drill or other disabling noise"}, 284 | {Match: []int{5}, Text: "Natural laser or plasma discharge"}, 285 | {Match: []int{6}, Text: "Nauseating stench or disabling chemical"}, 286 | {Match: []int{7}, Text: "Equipment-melting corrosive"}, 287 | {Match: []int{8}, Text: "Explosive pellets or chemical catalysts"}, 288 | }, 289 | } 290 | 291 | // Poisons 292 | 293 | // Poison p201 SWN:RE Free edition 294 | var poisonTable = struct { 295 | effect roll.Table 296 | onset roll.Table 297 | duration roll.Table 298 | }{ 299 | roll.Table{ 300 | Name: "Poison", 301 | ID: "beast.Poison", 302 | Dice: roll.Dice{N: 1, Die: roll.D6}, 303 | Items: []roll.TableItem{ 304 | {Match: []int{1}, Text: "Death"}, 305 | {Match: []int{2}, Text: "Paralysis"}, 306 | {Match: []int{3}, Text: "1d4 dmg per onset interval"}, 307 | {Match: []int{4}, Text: "Convulsions"}, 308 | {Match: []int{5}, Text: "Blindness"}, 309 | {Match: []int{6}, Text: "Hallucinations"}, 310 | }, 311 | }, 312 | roll.Table{ 313 | Name: "Onset", 314 | ID: "beast.Onset", 315 | Dice: roll.Dice{N: 1, Die: roll.D6}, 316 | Items: []roll.TableItem{ 317 | {Match: []int{1}, Text: "Instant"}, 318 | {Match: []int{2}, Text: "1 round"}, 319 | {Match: []int{3}, Text: "1d6 rounds"}, 320 | {Match: []int{4}, Text: "1 minute"}, 321 | {Match: []int{5}, Text: "1d6 minutes"}, 322 | {Match: []int{6}, Text: "1 hour"}, 323 | }, 324 | }, 325 | roll.Table{ 326 | Name: "Duration", 327 | ID: "beast.Duration", 328 | Dice: roll.Dice{N: 1, Die: roll.D6}, 329 | Items: []roll.TableItem{ 330 | {Match: []int{1}, Text: "1d6 rounds"}, 331 | {Match: []int{2}, Text: "1 minute"}, 332 | {Match: []int{3}, Text: "10 minutes"}, 333 | {Match: []int{4}, Text: "1 hour"}, 334 | {Match: []int{5}, Text: "1d6 hours"}, 335 | {Match: []int{6}, Text: "1d6 days"}, 336 | }, 337 | }, 338 | } 339 | 340 | func poisonAction() string { 341 | return fmt.Sprintf("in %s the target suffers from %s over %s.", 342 | poisonTable.onset.Roll(), 343 | poisonTable.effect.Roll(), 344 | poisonTable.duration.Roll()) 345 | } 346 | -------------------------------------------------------------------------------- /content/npc.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/nboughton/go-roll" 9 | "github.com/nboughton/swnt/content/culture" 10 | "github.com/nboughton/swnt/content/format" 11 | "github.com/nboughton/swnt/content/gender" 12 | "github.com/nboughton/swnt/content/name" 13 | "github.com/nboughton/swnt/content/table" 14 | ) 15 | 16 | func init() { 17 | table.Registry.Add(npcTable.D10.(roll.Table)) 18 | } 19 | 20 | // Patron is a Patron 21 | type Patron struct { 22 | Fields [][]string 23 | } 24 | 25 | // NewPatron just roll Patron details 26 | func NewPatron() Patron { 27 | return Patron{Fields: patronTable.Roll()} 28 | } 29 | 30 | // Format patron data as type t 31 | func (p Patron) Format(t format.OutputType) string { 32 | return format.Table(t, []string{"Patron", ""}, p.Fields) 33 | } 34 | 35 | // NPC represents an NPC 36 | type NPC struct { 37 | Name string 38 | Gender gender.Gender 39 | Culture culture.Culture 40 | Fields [][]string 41 | Hooks NPCHooks 42 | Patron Patron 43 | Reaction string 44 | } 45 | 46 | // NPCHooks character hooks, wants etc 47 | type NPCHooks struct { 48 | Manner string 49 | Outcome string 50 | Motivation string 51 | Want string 52 | Power string 53 | Hook string 54 | } 55 | 56 | // NewNPC roll a new NPC 57 | func NewNPC(ctr culture.Culture, g gender.Gender, isPatron bool) NPC { 58 | n := NPC{ 59 | Gender: g, 60 | Culture: ctr, 61 | Fields: npcTable.Roll(), 62 | Hooks: NPCHooks{ 63 | Manner: npcHooksTable.manner.Roll(), 64 | Outcome: npcHooksTable.outcome.Roll(), 65 | Motivation: npcHooksTable.motivation.Roll(), 66 | Want: npcHooksTable.want.Roll(), 67 | Power: npcHooksTable.power.Roll(), 68 | Hook: npcHooksTable.hook.Roll(), 69 | }, 70 | Reaction: Reaction.Roll(), 71 | } 72 | 73 | if isPatron { 74 | n.Patron = NewPatron() 75 | } 76 | 77 | nm := name.Table.ByCulture(ctr) 78 | switch g { 79 | case gender.Male: 80 | n.Name = fmt.Sprintf("%s %s", nm.Male.Roll(), nm.Surname.Roll()) 81 | case gender.Female: 82 | n.Name = fmt.Sprintf("%s %s", nm.Female.Roll(), nm.Surname.Roll()) 83 | case gender.Other, gender.Any: 84 | switch rand.Intn(2) { 85 | case 0: 86 | n.Name = fmt.Sprintf("%s %s", nm.Male.Roll(), nm.Surname.Roll()) 87 | case 1: 88 | n.Name = fmt.Sprintf("%s %s", nm.Female.Roll(), nm.Surname.Roll()) 89 | } 90 | } 91 | 92 | return n 93 | } 94 | 95 | // Format returns a string output in the specified format t 96 | func (n NPC) Format(t format.OutputType) string { 97 | buf := new(bytes.Buffer) 98 | 99 | fmt.Fprintf(buf, format.Table(t, []string{n.Name, ""}, [][]string{ 100 | {"Culture", n.Culture.String()}, 101 | {"Gender", n.Gender.String()}, 102 | })) 103 | fmt.Fprintf(buf, format.Table(t, []string{}, n.Fields)) 104 | fmt.Fprintf(buf, format.Table(t, []string{}, [][]string{ 105 | {"Hooks", ""}, 106 | {npcHooksTable.manner.Name, n.Hooks.Manner}, 107 | {npcHooksTable.outcome.Name, n.Hooks.Outcome}, 108 | {npcHooksTable.motivation.Name, n.Hooks.Motivation}, 109 | {npcHooksTable.want.Name, n.Hooks.Want}, 110 | {npcHooksTable.power.Name, n.Hooks.Power}, 111 | {npcHooksTable.hook.Name, n.Hooks.Hook}, 112 | {Reaction.Name, n.Reaction}, 113 | })) 114 | 115 | if len(n.Patron.Fields) > 0 { 116 | fmt.Fprintln(buf) 117 | fmt.Fprintf(buf, n.Patron.Format(t)) 118 | } 119 | 120 | return buf.String() 121 | } 122 | 123 | func (n NPC) String() string { 124 | return n.Format(format.TEXT) 125 | } 126 | 127 | // Reaction of possible reaction rolls for NPCs 128 | var Reaction = roll.Table{ 129 | Name: "Reaction Roll Results", 130 | Dice: roll.Dice{N: 2, Die: roll.D6}, 131 | Items: []roll.TableItem{ 132 | {Match: []int{2}, Text: "Hostile, reacting negatively as is plausible"}, 133 | {Match: []int{3, 4, 5}, Text: "Negative, unfriendly and unhelpful"}, 134 | {Match: []int{6, 7, 8}, Text: "Neutral, reacting predictably or warily"}, 135 | {Match: []int{9, 10, 11}, Text: "Positive, potentially cooperative with PCs"}, 136 | {Match: []int{12}, Text: "Friendly, helpful as is plausible to be"}, 137 | }, 138 | } 139 | 140 | var npcHooksTable = struct { 141 | manner roll.List 142 | outcome roll.List 143 | motivation roll.List 144 | want roll.List 145 | power roll.List 146 | hook roll.List 147 | }{ 148 | // Manner table 149 | roll.List{ 150 | Name: "Initial Manner", 151 | Items: []string{ 152 | "Ingratiating and cloying", 153 | "Grim suspicion of the PCs or their backers", 154 | "Xenophilic interest in the novelty of the PCs", 155 | "Pragmatic and businesslike", 156 | "Romantically interested in one or more PCs", 157 | "A slimy used-gravcar dealer’s approach", 158 | "Wide-eyed awe at the PCs", 159 | "Cool and superior attitude toward PC “hirelings”", 160 | "Benevolently patronizing toward outsiders", 161 | "Sweaty-palmed need or desperation", 162 | "Xenophobic mistrust of the PCs", 163 | "Idealistic enthusiasm for a potentially shared cause", 164 | "Somewhat intoxicated by recent indulgence", 165 | "Smoothly persuasive and reasonable", 166 | "Visibly uncomfortable with the PCs", 167 | "Grossly overconfident in PC abilities", 168 | "Somewhat frightened by the PCs", 169 | "Deeply misunderstanding the PCs’ culture", 170 | "Extremely well-informed about the PCs’ past", 171 | "Distracted by their current situation", 172 | }, 173 | }, 174 | 175 | // Outcome table 176 | roll.List{ 177 | Name: "Default Deal Outcome", 178 | Items: []string{ 179 | "They’ll screw the PCs over even at their own cost", 180 | "They firmly intend to actively betray the PCs", 181 | "They won’t keep the deal unless driven to it", 182 | "They plan to twist the deal to their own advantage", 183 | "They won’t keep their word unless it’s profitable", 184 | "They’ll flinch from paying up when the time comes", 185 | "They mean to keep the deal, but are reluctant", 186 | "They’ll keep most of the deal, but not all of it", 187 | "They’ll keep the deal slowly and grudgingly", 188 | "They’ll keep the deal but won’t go out of their way", 189 | "They’ll be reasonably punctual about the deal", 190 | "They’ll want a further small favor to pay up on it", 191 | "They’ll keep the deal in a way that helps them", 192 | "They’ll keep the deal if it’s still good for them", 193 | "They’ll offer a bonus for an additional favor", 194 | "Trustworthy as long as the deal won’t hurt them", 195 | "Trustworthy, with the NPC following through", 196 | "They’ll be very fair in keeping to their agreements", 197 | "They’ll keep bargains even to their own cost", 198 | "Complete and righteous integrity to the bitter end", 199 | }, 200 | }, 201 | 202 | // Motivation table 203 | roll.List{ 204 | Name: "Motivation", 205 | Items: []string{ 206 | "An ambition for greater social status", 207 | "Greed for wealth and indulgent riches", 208 | "Protect a loved one who is somehow imperiled", 209 | "A sheer sadistic love of inflicting pain and suffering", 210 | "Hedonistic enjoyment of pleasing company", 211 | "Searching out hidden knowledge or science", 212 | "Establishing or promoting a cultural institution", 213 | "Avenging a grievous wrong to them or a loved one", 214 | "Promoting their religion and living out their faith", 215 | "Winning the love of a particular person", 216 | "Winning glory and fame in their profession", 217 | "Dodging an enemy who is pursuing them", 218 | "Driving out or killing an enemy group", 219 | "Deposing a rival to them in their line of work", 220 | "Getting away from this world or society", 221 | "Promote a friend or offspring’s career or future", 222 | "Taking control of a property or piece of land", 223 | "Building a structure or a complex prototype tech", 224 | "Perform or create their art to vast acclaim", 225 | "Redeem themselves from a prior failure", 226 | }, 227 | }, 228 | 229 | // Want table 230 | roll.List{ 231 | Name: "Want", 232 | Items: []string{ 233 | "Bring them an exotic piece of tech", 234 | "Convince someone to meet with the NPC", 235 | "Kill a particular NPC", 236 | "Kidnap or non-fatally eliminate a particular NPC", 237 | "Pay them a large amount of money", 238 | "Take a message to someone hard to reach", 239 | "Acquire a tech component that’s hard to get", 240 | "Find proof of a particular NPC’s malfeasance", 241 | "Locate a missing NPC", 242 | "Bring someone to a destination via dangerous travel", 243 | "Retrieve a lost or stolen object", 244 | "Defend someone from an impending attack", 245 | "Burn down or destroy a particular structure", 246 | "Explore a dangerous or remote location", 247 | "Steal something from a rival NPC or group", 248 | "Intimidate a rival into ceasing their course of action", 249 | "Commit a minor crime to aid the NPC", 250 | "Trick a rival into doing something", 251 | "Rescue an NPC from a dire situation", 252 | "Force a person or group to leave an area", 253 | }, 254 | }, 255 | 256 | // Power table 257 | roll.List{ 258 | Name: "Power", 259 | Items: []string{ 260 | "They’re just really appealing and sympathetic to PCs", 261 | "They have considerable liquid funds", 262 | "They control the use of large amounts of violence", 263 | "They have a position of great social status", 264 | "They’re a good friend of an important local leader", 265 | "They have blackmail info on the PCs", 266 | "They have considerable legal influence here", 267 | "They have tech the PCs might reasonably want", 268 | "They can get the PCs into a place they want to go", 269 | "They know where significant wealth can be found", 270 | "They have information about the PCs’ current goal", 271 | "An NPC the PCs need has implicit trust in them", 272 | "The NPC can threaten someone the PCs like", 273 | "They control a business relevant to PC needs", 274 | "They have considerable criminal contacts", 275 | "They have pull with the local religion", 276 | "They know a great many corrupt politicians", 277 | "They can alert the PCs to an unexpected peril", 278 | "They’re able to push a goal the PCs currently have", 279 | "They can get the PCs useful permits and rights", 280 | }, 281 | }, 282 | 283 | // Hook table 284 | roll.List{ 285 | Name: "Hook", 286 | Items: []string{ 287 | "A particular odd style of dress", 288 | "An amputation or other maiming", 289 | "Visible cyberware or prosthetics", 290 | "Unusual hair, skin, or eye colors", 291 | "Scarring, either intentional or from old injuries", 292 | "Tic-like overuse of a particular word or phrase", 293 | "Specific unusual fragrance or cologne", 294 | "Constant fiddling with a particular item", 295 | "Visible signs of drug use", 296 | "Always seems to be in one particular mood", 297 | "Wears badges or marks of allegiance to a cause", 298 | "Extremely slow or fast pace of speech", 299 | "Wheezes, shakes, or other signs of infirmity", 300 | "Constantly with a drink to hand", 301 | "Always complaining about a group or organization", 302 | "Paranoid, possibly for justifiable reasons", 303 | "Insists on a particular location for all meetings", 304 | "Communicates strictly through a third party", 305 | "Abnormally obese, emaciated, tall, or short", 306 | "Always found with henchmen or friends", 307 | }, 308 | }, 309 | } 310 | 311 | // PatronTable represents the tables to roll on to create an adventure Patron 312 | var patronTable = table.OneRoll{ 313 | D4: roll.List{ 314 | Name: "Patron Eagerness to Hire", 315 | Items: []string{ 316 | "Cautious, but can be convinced to hire", 317 | "Willing to promise standard rates", 318 | "Eager, willing to offer a bonus", 319 | "Desperate, might offer what they can’t pay", 320 | }, 321 | }, 322 | D6: roll.List{ 323 | Name: "Patron Trustworthiness", 324 | Items: []string{ 325 | "They intend to totally screw the PCs", 326 | "They won’t pay unless forced to do so", 327 | "They’ll pay slowly or reluctantly", 328 | "They’ll pay, but discount for mistakes", 329 | "They’ll pay without quibbling", 330 | "They’ll pay more than they promised", 331 | }, 332 | }, 333 | D8: roll.List{ 334 | Name: "Basic Challenge of the Job", 335 | Items: []string{ 336 | "Kill somebody who might deserve it", 337 | "Kidnap someone dangerous", 338 | "Steal a well-guarded object", 339 | "Arson or sabotage on a place", 340 | "Get proof of some misdeed", 341 | "Protect someone from an immediate threat", 342 | "Transport someone through danger", 343 | "Guard an object being transported", 344 | }, 345 | }, 346 | D10: roll.List{ 347 | Name: "Main Countervailing Force", 348 | Items: []string{ 349 | "A treacherous employer or subordinate", 350 | "An open and known enemy of the patron", 351 | "Official governmental meddling", 352 | "An unknown rival of the patron", 353 | "The macguffin itself opposes them", 354 | "Very short time frame allowed", 355 | "The job is spectacularly illegal", 356 | "A participant would profit by their failure", 357 | "The patron is badly wrong about a thing", 358 | "The locals are against the patron", 359 | }, 360 | }, 361 | D12: roll.List{ 362 | Name: "Potential Non-Cash Rewards", 363 | Items: []string{ 364 | "Government official favors owed", 365 | "Property in the area", 366 | "An item very valuable on another world", 367 | "Pretech mod components", 368 | "Useful pretech artifact", 369 | "Information the PCs need", 370 | "Membership in a powerful group", 371 | "Black market access", 372 | "Use of restricted facilities or shipyards", 373 | "Shares in a profitable business", 374 | "Maps to a hidden or guarded treasure", 375 | "Illegal but valuable weapons or gear", 376 | }, 377 | }, 378 | D20: roll.List{ 379 | Name: "Complication to the Job", 380 | Items: []string{ 381 | "An ambush is laid somewhere", 382 | "PC involvement is leaked to the enemy", 383 | "The patron gives faulty aid somehow", 384 | "Failing would be extremely unhealthy", 385 | "The job IDs them as allies of a local faction", 386 | "The macguffin is physically dangerous", 387 | "An important location is hard to get into", 388 | "Succeeding would be morally distasteful", 389 | "A supposed ally is very unhelpful or stupid", 390 | "The patron badly misunderstood the PCs", 391 | "The job changes suddenly partway through", 392 | "An unexpected troublemaker is involved", 393 | "Critical gear will fail partway through", 394 | "An unrelated accident complicates things", 395 | "Payment comes in a hard-to-handle form", 396 | "Someone is turning traitor on the patron", 397 | "A critical element has suddenly moved", 398 | "Payment is in avidly-pursued hot goods", 399 | "The true goal is a subsidiary part of the job", 400 | "No complications; it’s just as it seems to be", 401 | }, 402 | }, 403 | } 404 | 405 | // NPCTable represents the tables to roll on to create an NPC 406 | var npcTable = table.OneRoll{ 407 | D4: roll.List{ 408 | Name: "Age", 409 | Items: []string{ 410 | "Unusually young or old for their role", 411 | "Young adult", 412 | "Mature prime", 413 | "Middle-aged or elderly", 414 | }, 415 | }, 416 | D6: roll.Table{ 417 | Name: "Background", 418 | Dice: roll.Dice{N: 1, Die: roll.D6}, 419 | Items: []roll.TableItem{ 420 | {Match: []int{1}, Text: "The local underclass or poorest natives"}, 421 | {Match: []int{2}, Text: "Common laborers or cube workers"}, 422 | {Match: []int{3}, Text: "Aspiring bourgeoise or upper class"}, 423 | {Match: []int{4}, Text: "The elite of this society"}, 424 | {Match: []int{5}, Text: "Minority or foreigners"}, 425 | {Match: []int{6}, Text: "Offworlders or exotics"}, 426 | }, 427 | Reroll: roll.TableReroll{ 428 | Dice: roll.Dice{N: 1, Die: roll.D4}, 429 | Match: []int{5, 6}, 430 | }, 431 | }, 432 | D8: roll.List{ 433 | Name: "Role in Society", 434 | Items: []string{ 435 | "Criminal, thug, thief, swindler", 436 | "Menial, cleaner, retail worker, servant", 437 | "Unskilled heavy labor, porter, construction", 438 | "Skilled trade, electrician, mechanic, pilot", 439 | "Idea worker, programmer, writer", 440 | "Merchant, business owner, trader, banker", 441 | "Official, bureaucrat, courtier, clerk", 442 | "Military, soldier, enforcer, law officer", 443 | }, 444 | }, 445 | D10: roll.Table{ 446 | Name: "Biggest Problem", 447 | ID: "npc.BiggestProblem", 448 | Dice: roll.Dice{N: 1, Die: roll.D10}, 449 | Items: []roll.TableItem{ 450 | {Match: []int{1}, Text: "They have significant debt or money woes"}, 451 | {Match: []int{2}, Text: "A loved one is in trouble", Action: func() string { 452 | tbl, _ := table.Registry.Get("npc.BiggestProblem") 453 | 454 | var res string 455 | for res = tbl.Roll(); res != tbl.Items[1].Text; res = tbl.Roll() { 456 | return res 457 | } 458 | return "" 459 | }}, 460 | {Match: []int{3}, Text: "Romantic failure with a desired person"}, 461 | {Match: []int{4}, Text: "Drug or behavioral addiction"}, 462 | {Match: []int{5}, Text: "Their superior dislikes or resents them"}, 463 | {Match: []int{6}, Text: "They have a persistent sickness"}, 464 | {Match: []int{7}, Text: "They hate their job or life situation"}, 465 | {Match: []int{8}, Text: "Someone dangerous is targeting them"}, 466 | {Match: []int{9}, Text: "They’re pursuing a disastrous purpose"}, 467 | {Match: []int{10}, Text: "They have no problems worth mentioning"}, 468 | }, 469 | }, 470 | D12: roll.List{ 471 | Name: "Greatest Desire", 472 | Items: []string{ 473 | "They want a particular romantic partner", 474 | "They want money for them or a loved one", 475 | "They want a promotion in their job", 476 | "They want answers about a past trauma", 477 | "They want revenge on an enemy", 478 | "They want to help a beleaguered friend", 479 | "They want an entirely different job", 480 | "They want protection from an enemy", 481 | "They want to leave their current life", 482 | "They want fame and glory", 483 | "They want power over those around them", 484 | "They have everything they want from life", 485 | }, 486 | }, 487 | D20: roll.List{ 488 | Name: "Most Obvious Character Trait", 489 | Items: []string{ 490 | "Ambition", 491 | "Avarice", 492 | "Bitterness", 493 | "Courage", 494 | "Cowardice", 495 | "Curiosity", 496 | "Deceitfulness", 497 | "Determination", 498 | "Devotion to a cause", 499 | "Filiality", 500 | "Hatred", 501 | "Honesty", 502 | "Hopefulness", 503 | "Love of a person", 504 | "Nihilism", 505 | "Paternalism", 506 | "Pessimism", 507 | "Protectiveness", 508 | "Resentment", 509 | "Shame", 510 | }, 511 | }, 512 | } 513 | -------------------------------------------------------------------------------- /content/name/name.go: -------------------------------------------------------------------------------- 1 | // Package name provides tables and a generator for NPC and place names 2 | package name 3 | 4 | import ( 5 | "github.com/nboughton/go-roll" 6 | "github.com/nboughton/swnt/content/culture" 7 | ) 8 | 9 | // table represents the collection of roll lists keyed by cultural background 10 | type table struct { 11 | Culture culture.Culture 12 | Male roll.List 13 | Female roll.List 14 | Surname roll.List 15 | Place roll.List 16 | } 17 | 18 | // Tables represents a set of table structs 19 | type tables []table 20 | 21 | // ByCulture returns a name table that matches the given culture 22 | func (t tables) ByCulture(c culture.Culture) table { 23 | if c == culture.Any { 24 | c = culture.Random() 25 | } 26 | 27 | for _, tbl := range t { 28 | if tbl.Culture == c { 29 | return tbl 30 | } 31 | } 32 | 33 | return table{} 34 | } 35 | 36 | // Table represents rollable tables for individuals names 37 | var Table = tables{ 38 | table{ 39 | Culture: culture.Arabic, 40 | Male: roll.List{Items: []string{"Aamir", "Ayub", "Binyamin", "Efraim", "Ibrahim", "Ilyas", "Ismail", "Jibril", "Jumanah", "Kazi", "Lut", "Matta", "Mohammed", "Mubarak", "Mustafa", "Nazir", "Rahim", "Reza", "Sharif", "Taimur", "Usman", "Yakub", "Yusuf", "Zakariya", "Zubair"}}, 41 | Female: roll.List{Items: []string{"Aisha", "Alimah", "Badia", "Bisharah", "Chanda", "Daliya", "Fatimah", "Ghania", "Halah", "Kaylah", "Khayrah", "Layla", "Mina", "Munisa", "Mysha", "Naimah", "Nissa", "Nura", "Parveen", "Rana", "Shalha", "Suhira", "Tahirah", "Yasmin", "Zulehka"}}, 42 | Surname: roll.List{Items: []string{"Abdel", "Awad", "Dahhak", "Essa", "Hanna", "Harbi", "Hassan", "Isa", "Kasim", "Katib", "Khalil", "Malik", "Mansoor", "Mazin", "Musa", "Najeeb", "Namari", "Naser", "Rahman", "Rasheed", "Saleh", "Salim", "Shadi", "Sulaiman", "Tabari"}}, 43 | Place: roll.List{Items: []string{"Adan", "Magrit", "Ahsa", "Masqat", "Andalus", "Misr", "Asmara", "Muruni", "Asqlan", "Qabis", "Baqubah", "Qina", "Basit", "Rabat", "Baysan", "Ramlah", "Baytlahm", "Riyadh", "Bursaid", "Sabtah", "Dahilah", "Salalah", "Darasalam", "Sana", "Dawhah", "Sinqit", "Ganin", "Suqutrah", "Gebal", "Sur", "Gibuti", "Tabuk", "Giddah", "Tangah", "Harmah", "Tarifah", "Hartum", "Tarrakunah", "Hibah", "Tisit", "Hims", "Uman", "Hubar", "Urdunn", "Karbala", "Wasqah", "Kut", "Yaburah", "Lacant", "Yaman"}}, 44 | }, 45 | table{ 46 | Culture: culture.Chinese, 47 | Male: roll.List{Items: []string{"Aiguo", "Bohai", "Chao", "Dai", "Dawei", "Duyi", "Fa", "Fu", "Gui", "Hong", "Jianyu", "Kang", "Li", "Niu", "Peng", "Quan", "Ru", "Shen", "Shi", "Song", "Tao", "Xue", "Yi", "Yuan", "Zian"}}, 48 | Female: roll.List{Items: []string{"Biyu", "Changying", "Daiyu", "Huidai", "Huiliang", "Jia", "Jingfei", "Lan", "Liling", "Liu", "Meili", "Niu", "Peizhi", "Qiao", "Qing", "Ruolan", "Shu", "Suyin", "Ting", "Xia", "Xiaowen", "Xiulan", "Ya", "Ying", "Zhilan"}}, 49 | Surname: roll.List{Items: []string{"Bai", "Cao", "Chen", "Cui", "Ding", "Du", "Fang", "Fu", "Guo", "Han", "Hao", "Huang", "Lei", "Li", "Liang", "Liu", "Long", "Song", "Tan", "Tang", "Wang", "Wu", "Xing", "Yang", "Zhang"}}, 50 | Place: roll.List{Items: []string{"Andong", "Luzhou", "Anqing", "Ningxia", "Anshan", "Pingxiang", "Chaoyang", "Pizhou", "Chaozhou", "Qidong", "Chifeng", "Qingdao", "Dalian", "Qinghai", "Dunhuang", "Rehe", "Fengjia", "Shanxi", "Fengtian", "Taiyuan", "Fuliang", "Tengzhou", "Fushun", "Urumqi", "Gansu", "Weifang", "Ganzhou", "Wugang", "Guizhou", "Wuxi", "Hotan", "Xiamen", "Hunan", "Xian", "Jinan", "Xikang", "Jingdezhen", "Xining", "Jinxi", "Xinjiang", "Jinzhou", "Yidu", "Kunming", "Yingkou", "Liaoning", "Yuxi", "Linyi", "Zigong", "Lushun", "Zoige"}}, 51 | }, 52 | table{ 53 | Culture: culture.English, 54 | Male: roll.List{Items: []string{"Adam", "Albert", "Alfred", "Allan", "Archibald", "Arthur", "Basil", "Charles", "Colin", "Donald", "Douglas", "Edgar", "Edmund", "Edward", "George", "Harold", "Henry", "Ian", "James", "John", "Lewis", "Oliver", "Philip", "Richard", "William"}}, 55 | Female: roll.List{Items: []string{"Abigail", "Anne", "Beatrice", "Blanche", "Catherine", "Charlotte", "Claire", "Eleanor", "Elizabeth", "Emily", "Emma", "Georgia", "Harriet", "Joan", "Judy", "Julia", "Lucy", "Lydia", "Margaret", "Mary", "Molly", "Nora", "Rosie", "Sarah", "Victoria"}}, 56 | Surname: roll.List{Items: []string{"Barker", "Brown", "Butler", "Carter", "Chapman", "Collins", "Cook", "Davies", "Gray", "Green", "Harris", "Jackson", "Jones", "Lloyd", "Miller", "Roberts", "Smith", "Taylor", "Thomas", "Turner", "Watson", "White", "Williams", "Wood", "Young"}}, 57 | Place: roll.List{Items: []string{"Aldington", "Kedington", "Appleton", "Latchford", "Ashdon", "Leigh", "Berwick", "Leighton", "Bramford", "Maresfield", "Brimstage", "Markshall", "Carden", "Netherpool", "Churchill", "Newton", "Clifton", "Oxton", "Colby", "Preston", "Copford", "Ridley", "Cromer", "Rochford", "Davenham", "Seaford", "Dersingham", "Selsey", "Doverdale", "Stanton", "Elsted", "Stockham", "Ferring", "Stoke", "Gissing", "Sutton", "Heydon", "Thakeham", "Holt", "Thetford", "Hunston", "Thorndon", "Hutton", "Ulting", "Inkberrow", "Upton", "Inworth", "Westhorpe", "Isfield", "Worcester"}}, 58 | }, 59 | table{ 60 | Culture: culture.Greek, 61 | Male: roll.List{Items: []string{"Alexander", "Alexius", "Anastasius", "Christodoulos", "Christos", "Damian", "Dimitris", "Dysmas", "Elias", "Giorgos", "Ioannis", "Konstantinos", "Lambros", "Leonidas", "Marcos", "Miltiades", "Nestor", "Nikos", "Orestes", "Petros", "Simon", "Stavros", "Theodore", "Vassilios", "Yannis"}}, 62 | Female: roll.List{Items: []string{"Alexandra", "Amalia", "Callisto", "Charis", "Chloe", "Dorothea", "Elena", "Eudoxia", "Giada", "Helena", "Ioanna", "Lydia", "Melania", "Melissa", "Nika", "Nikolina", "Olympias", "Philippa", "Phoebe", "Sophia", "Theodora", "Valentina", "Valeria", "Yianna", "Zoe"}}, 63 | Surname: roll.List{Items: []string{"Andreas", "Argyros", "Dimitriou", "Floros", "Gavras", "Ioannidis", "Katsaros", "Kyrkos", "Leventis", "Makris", "Metaxas", "Nikolaidis", "Pallis", "Pappas", "Petrou", "Raptis", "Simonides", "Spiros", "Stavros", "Stephanidis", "Stratigos", "Terzis", "Theodorou", "Vasiliadis", "Yannakakis"}}, 64 | Place: roll.List{Items: []string{"Adramyttion", "Kallisto", "Ainos", "Katerini", "Alikarnassos", "Kithairon", "Avydos", "Kydonia", "Dakia", "Lakonia", "Dardanos", "Leros", "Dekapoli", "Lesvos", "Dodoni", "Limnos", "Efesos", "Lykia", "Efstratios", "Megara", "Elefsina", "Messene", "Ellada", "Milos", "Epidavros", "Nikaia", "Erymanthos", "Orontis", "Evripos", "Parnasos", "Gavdos", "Petro", "Gytheio", "Samos", "Ikaria", "Syros", "Ilios", "Thapsos", "Illyria", "Thessalia", "Iraia", "Thira", "Irakleio", "Thiva", "Isminos", "Varvara", "Ithaki", "Voiotia", "Kadmeia", "Vyvlos"}}, 65 | }, 66 | table{ 67 | Culture: culture.Indian, 68 | Male: roll.List{Items: []string{"Amrit", "Ashok", "Chand", "Dinesh", "Gobind", "Harinder", "Jagdish", "Johar", "Kurien", "Lakshman", "Madhav", "Mahinder", "Mohal", "Narinder", "Nikhil", "Omrao", "Prasad", "Pratap", "Ranjit", "Sanjay", "Shankar", "Thakur", "Vijay", "Vipul", "Yash"}}, 69 | Female: roll.List{Items: []string{"Amala", "Asha", "Chandra", "Devika", "Esha", "Gita", "Indira", "Indrani", "Jaya", "Jayanti", "Kiri", "Lalita", "Malati", "Mira", "Mohana", "Neela", "Nita", "Rajani", "Sarala", "Sarika", "Sheela", "Sunita", "Trishna", "Usha", "Vasanta"}}, 70 | Surname: roll.List{Items: []string{"Achari", "Banerjee", "Bhatnagar", "Bose", "Chauhan", "Chopra", "Das", "Dutta", "Gupta", "Johar", "Kapoor", "Mahajan", "Malhotra", "Mehra", "Nehru", "Patil", "Rao", "Saxena", "Shah", "Sharma", "Singh", "Trivedi", "Venkatesan", "Verma", "Yadav"}}, 71 | Place: roll.List{Items: []string{"Ahmedabad", "Jaisalmer", "Alipurduar", "Jharonda", "Alubari", "Kadambur", "Anjanadri", "Kalasipalyam", "Ankleshwar", "Karnataka", "Balarika", "Kutchuhery", "Bhanuja", "Lalgola", "Bhilwada", "Mainaguri", "Brahmaghosa", "Nainital", "Bulandshahar", "Nandidurg", "Candrama", "Narayanadri", "Chalisgaon", "Panipat", "Chandragiri", "Panjagutta", "Charbagh", "Pathankot", "Chayanka", "Pathardih", "Chittorgarh", "Porbandar", "Dayabasti", "Rajasthan", "Dikpala", "Renigunta", "Ekanga", "Sewagram", "Gandhidham", "Shakurbasti", "Gollaprolu", "Siliguri", "Grahisa", "Sonepat", "Guwahati", "Teliwara", "Haridasva", "Tinpahar", "Indraprastha", "Villivakkam"}}, 72 | }, 73 | table{ 74 | Culture: culture.Japanese, 75 | Male: roll.List{Items: []string{"Akira", "Daisuke", "Fukashi", "Goro", "Hiro", "Hiroya", "Hotaka", "Katsu", "Katsuto", "Keishuu", "Kyuuto", "Mikiya", "Mitsunobu", "Mitsuru", "Naruhiko", "Nobu", "Shigeo", "Shigeto", "Shou", "Shuji", "Takaharu", "Teruaki", "Tetsushi", "Tsukasa", "Yasuharu"}}, 76 | Female: roll.List{Items: []string{"Aemi", "Airi", "Ako", "Ayu", "Chikaze", "Eriko", "Hina", "Kaori", "Keiko", "Kyouka", "Mayumi", "Miho", "Namiko", "Natsu", "Nobuko", "Rei", "Ririsa", "Sakimi", "Shihoko", "Shika", "Tsukiko", "Tsuzune", "Yoriko", "Yorimi", "Yoshiko"}}, 77 | Surname: roll.List{Items: []string{"Abe", "Arakaki", "Endo", "Fujiwara", "Goto", "Ito", "Kikuchi", "Kinjo", "Kobayashi", "Koga", "Komatsu", "Maeda", "Nakamura", "Narita", "Ochi", "Oshiro", "Saito", "Sakamoto", "Sato", "Suzuki", "Takahashi", "Tanaka", "Watanabe", "Yamamoto", "Yamasaki"}}, 78 | Place: roll.List{Items: []string{"Bando", "Mitsukaido", "Chikuma", "Moriya", "Chikusei", "Nagano", "Chino", "Naka", "Hitachi", "Nakano", "Hitachinaka", "Ogi", "Hitachiomiya", "Okaya", "Hitachiota", "Omachi", "Iida", "Ryugasaki", "Iiyama", "Saku", "Ina", "Settsu", "Inashiki", "Shimotsuma", "Ishioka", "Shiojiri", "Itako", "Suwa", "Kamisu", "Suzaka", "Kasama", "Takahagi", "Kashima", "Takeo", "Kasumigaura", "Tomi", "Kitaibaraki", "Toride", "Kiyose", "Tsuchiura", "Koga", "Tsukuba", "Komagane", "Ueda", "Komoro", "Ushiku", "Matsumoto", "Yoshikawa", "Mito", "Yuki"}}, 79 | }, 80 | table{ 81 | Culture: culture.Latin, 82 | Male: roll.List{Items: []string{"Agrippa", "Appius", "Aulus", "Caeso", "Decimus", "Faustus", "Gaius", "Gnaeus", "Hostus", "Lucius", "Mamercus", "Manius", "Marcus", "Mettius", "Nonus", "Numerius", "Opiter", "Paulus", "Proculus", "Publius", "Quintus", "Servius", "Tiberius", "Titus", "Volescus"}}, 83 | Female: roll.List{Items: []string{"Appia", "Aula", "Caesula", "Decima", "Fausta", "Gaia", "Gnaea", "Hosta", "Lucia", "Maio", "Marcia", "Maxima", "Mettia", "Nona", "Numeria", "Octavia", "Postuma", "Prima", "Procula", "Septima", "Servia", "Tertia", "Tiberia", "Titia", "Vibia"}}, 84 | Surname: roll.List{Items: []string{"Antius", "Aurius", "Barbatius", "Calidius", "Cornelius", "Decius", "Fabius", "Flavius", "Galerius", "Horatius", "Julius", "Juventius", "Licinius", "Marius", "Minicius", "Nerius", "Octavius", "Pompeius", "Quinctius", "Rutilius", "Sextius", "Titius", "Ulpius", "Valerius", "Vitellius"}}, 85 | Place: roll.List{Items: []string{"Abilia", "Lucus", "Alsium", "Lugdunum", "Aquileia", "Mediolanum", "Argentoratum", "Novaesium", "Ascrivium", "Patavium", "Asculum", "Pistoria", "Attalia", "Pompeii", "Barium", "Raurica", "Batavorum", "Rigomagus", "Belum", "Roma", "Bobbium", "Salernum", "Brigantium", "Salona", "Burgodunum", "Segovia", "Camulodunum", "Sirmium", "Clausentum", "Spalatum", "Corduba", "Tarraco", "Coriovallum", "Treverorum", "Durucobrivis", "Verulamium", "Eboracum", "Vesontio", "Emona", "Vetera", "Florentia", "Vindelicorum", "Lactodurum", "Vindobona", "Lentia", "Vinovia", "Lindum", "Viroconium", "Londinium", "Volubilis"}}, 86 | }, 87 | table{ 88 | Culture: culture.Nigerian, 89 | Male: roll.List{Items: []string{"Adesegun", "Akintola", "Amabere", "Arikawe", "Asagwara", "Chidubem", "Chinedu", "Chiwetei", "Damilola", "Esangbedo", "Ezenwoye", "Folarin", "Genechi", "Idowu", "Kelechi", "Ketanndu", "Melubari", "Nkanta", "Obafemi", "Olatunde", "Olumide", "Tombari", "Udofia", "Uyoata", "Uzochi"}}, 90 | Female: roll.List{Items: []string{"Abike", "Adesuwa", "Adunola", "Anguli", "Arewa", "Asari", "Bisola", "Chioma", "Eduwa", "Emilohi", "Fehintola", "Folasade", "Mahparah", "Minika", "Nkolika", "Nkoyo", "Nuanae", "Obioma", "Olafemi", "Shanumi", "Sominabo", "Suliat", "Tariere", "Temedire", "Yemisi"}}, 91 | Surname: roll.List{Items: []string{"Adegboye", "Adeniyi", "Adeyeku", "Adunola", "Agbaje", "Akpan", "Akpehi", "Aliki", "Asuni", "Babangida", "Ekim", "Ezeiruaku", "Fabiola", "Fasola", "Nwokolo", "Nzeocha", "Ojo", "Okonkwo", "Okoye", "Olaniyan", "Olawale", "Olumese", "Onajobi", "Soyinka", "Yamusa"}}, 92 | Place: roll.List{Items: []string{"Abadan", "Jere", "Ador", "Kalabalge", "Agatu", "Katsina", "Akamkpa", "Knoduga", "Akpabuyo", "Konshishatse", "Ala", "Kukawa", "Askira", "Kwande", "Bakassi", "Kwayakusar", "Bama", "Logo", "Bayo", "Mafa", "Bekwara", "Makurdi", "Biase", "Nganzai", "Boki", "Obanliku", "Buruku", "Obi", "Calabar", "Obubra", "Chibok", "Obudu", "Damboa", "Odukpani", "Dikwa", "Ogbadibo", "Etung", "Ohimini", "Gboko", "Okpokwu", "Gubio", "Otukpo", "Guzamala", "Shani", "Gwoza", "Ugep", "Hawul", "Vandeikya", "Ikom", "Yala"}}, 93 | }, 94 | table{ 95 | Culture: culture.Russian, 96 | Male: roll.List{Items: []string{"Aleksandr", "Andrei", "Arkady", "Boris", "Dmitri", "Dominik", "Grigory", "Igor", "Ilya", "Ivan", "Kiril", "Konstantin", "Leonid", "Nikolai", "Oleg", "Pavel", "Petr", "Sergei", "Stepan", "Valentin", "Vasily", "Viktor", "Yakov", "Yegor", "Yuri"}}, 97 | Female: roll.List{Items: []string{"Aleksandra", "Anastasia", "Anja", "Catarina", "Devora", "Dima", "Ekaterina", "Eva", "Irina", "Karolina", "Katlina", "Kira", "Ludmilla", "Mara", "Nadezdha", "Nastassia", "Natalya", "Oksana", "Olena", "Olga", "Sofia", "Svetlana", "Tatyana", "Vilma", "Yelena"}}, 98 | Surname: roll.List{Items: []string{"Abelev", "Bobrikov", "Chemerkin", "Gogunov", "Gurov", "Iltchenko", "Kavelin", "Komarov", "Korovin", "Kurnikov", "Lebedev", "Litvak", "Mekhdiev", "Muraviov", "Nikitin", "Ortov", "Peshkov", "Romasko", "Shvedov", "Sikorski", "Stolypin", "Turov", "Volokh", "Zaitsev", "Zhukov"}}, 99 | Place: roll.List{Items: []string{"Amur", "Omsk", "Arkhangelsk", "Orenburg", "Astrakhan", "Oryol", "Belgorod", "Penza", "Bryansk", "Perm", "Chelyabinsk", "Pskov", "Chita", "Rostov", "Gorki", "Ryazan", "Irkutsk", "Sakhalin", "Ivanovo", "Samara", "Kaliningrad", "Saratov", "Kaluga", "Smolensk", "Kamchatka", "Sverdlovsk", "Kemerovo", "Tambov", "Kirov", "Tomsk", "Kostroma", "Tula", "Kurgan", "Tver", "Kursk", "Tyumen", "Leningrad", "Ulyanovsk", "Lipetsk", "Vladimir", "Magadan", "Volgograd", "Moscow", "Vologda", "Murmansk", "Voronezh", "Novgorod", "Vyborg", "Novosibirsk", "Yaroslavl"}}, 100 | }, 101 | table{ 102 | Culture: culture.Spanish, 103 | Male: roll.List{Items: []string{"Alejandro", "Alonso", "Amelio", "Armando", "Bernardo", "Carlos", "Cesar", "Diego", "Emilio", "Estevan", "Felipe", "Francisco", "Guillermo", "Javier", "Jose", "Juan", "Julio", "Luis", "Pedro", "Raul", "Ricardo", "Salvador", "Santiago", "Valeriano", "Vicente"}}, 104 | Female: roll.List{Items: []string{"Adalina", "Aleta", "Ana", "Ascencion", "Beatriz", "Carmela", "Celia", "Dolores", "Elena", "Emelina", "Felipa", "Inez", "Isabel", "Jacinta", "Lucia", "Lupe", "Maria", "Marta", "Nina", "Paloma", "Rafaela", "Soledad", "Teresa", "Valencia", "Zenaida"}}, 105 | Surname: roll.List{Items: []string{"Arellano", "Arispana", "Borrego", "Carderas", "Carranzo", "Cordova", "Enciso", "Espejo", "Gavilan", "Guerra", "Guillen", "Huertas", "Illan", "Jurado", "Moretta", "Motolinia", "Pancorbo", "Paredes", "Quesada", "Roma", "Rubiera", "Santoro", "Torrillas", "Vera", "Vivero"}}, 106 | Place: roll.List{Items: []string{"Aguascebas", "Loreto", "Alcazar", "Lujar", "Barranquete", "Marbela", "Bravatas", "Matagorda", "Cabezudos", "Nacimiento", "Calderon", "Niguelas", "Cantera", "Ogijares", "Castillo", "Ortegicar", "Delgadas", "Pampanico", "Donablanca", "Pelado", "Encinetas", "Quesada", "Estrella", "Quintera", "Faustino", "Riguelo", "Fuentebravia", "Ruescas", "Gafarillos", "Salteras", "Gironda", "Santopitar", "Higueros", "Taberno", "Huelago", "Torres", "Humilladero", "Umbrete", "Illora", "Valdecazorla", "Isabela", "Velez", "Izbor", "Vistahermosa", "Jandilla", "Yeguas", "Jinetes", "Zahora", "Limones", "Zumeta"}}, 107 | }, 108 | } 109 | 110 | // System is a list of possible star system names taken from a wikipedia page on fictional planet names. 111 | var System = roll.List{Items: []string{"Abyormen", "Acheron", "Aegus", "Aether", "Ahnooie-4", "Aiur", "Aka", "Alcarinque", "Aldeian", "Alderaan", "Alkarinque", "Alpha", "Altair IV", "Alwas", "Amanga", "Amazo", "Ambar", "Anarres", "Anubis", "Aquarius", "Aquas", "Arcadia", "Arda", "Arisia", "Ark", "Arlia", "Armaghast", "Arrakis", "Astra", "Athos", "Athshe", "Atlantis", "Aurelia", "Auron", "Axturias", "Azeroth", "Baab", "Bajor", "Balaho", "Baloris", "Baltan", "Barathrum", "Barrayar", "Barsoom", "Bas-Lag", "Bazoik", "Beezee", "Belzagor", "Beowulf", "Bespin", "Beulah", "Bismoll", "Black Star", "Blue Sands", "Bog", "Bop", "Boskone", "Braal", "Brontitall", "Bryyo", "Cadwal", "Caladan", "Calafia", "Camazotz", "Caprica", "Carnil", "Carnivalia", "Centauri", "Cetaganda", "Chel", "Chthon", "Clin", "Corneria", "Coruscant", "Covenant", "Crematoria", "Crete", "Cybertron", "Cygnus Alpha", "Cyteen", "Dada", "Dagobah", "Darkover", "Dar Sai", "Daxam", "Dyan", "Deemi", "Demeter", "Denzi", "Dezoris", "Dhrawn", "Diso", "Doisac", "Dorsai", "Dosadi", "Dragon's Egg", "Dragon", "Dres", "Druidia", "Dryad", "Dump", "Duna", "Durdane", "Ea", "Earendil", "Eayn", "Echronedal", "Eeloo", "Elemmire", "Ellicoore 2", "Elysia", "Emerald", "Empyrrean", "Endor", "Epsilon 3", "Erna", "Eve", "Expel", "Exxilon", "Eylor", "Famille", "Fanbelt", "Far Away", "Fargett", "Fhloston", "Fichina", "Fiorina", "Flash", "Fortuna", "Freeza 79", "Fribbulus Xax", "Friedland", "Frystaat", "Galaxian 3", "Gallifrey", "Gamilon", "Gamilus", "Garissa", "Garrota", "Garth", "Gauda Prime", "Gaul", "Gelidus", "Genesis", "Gethen", "Giedi Prime", "Giganda", "Girath", "Gloob", "Gnarlach", "Gnosticus IV", "Gobotron", "God's Grove", "Golgota", "Gor", "Gorgona", "Gork", "Grayson", "Groth", "Gurun", "Gyodai", "Hades", "Hain", "Halvmork", "Harvest", "Hazard", "Heath", "Hebron", "Hegira", "Hekla", "Helicon", "Helion Prime", "Helliconia", "Hiigara", "Hikari", "Home", "Hope", "Horizon", "Hoth", "Houston", "Htrae", "Hummard", "Hyaita 4", "Hydros", "Hydross", "Hyperion", "Iga", "Ilu", "Imbar", "Incandescent", "Interchange", "Ireta", "Irk", "Ivy", "Ix", "Ixchel", "Janjur Qom", "Jijo", "Jinx", "Jobis", "Jool", "Jophekka", "Jurai", "Kaitain", "Kalimdor", "Kamino", "Kanassa", "Karn", "Kashyyyk", "Katina", "Kelewan", "Kerbin", "Kharak", "King", "Kithrup", "Klaus", "Klendathu", "Kobaia", "Kobol", "Komarr", "Koozebane", "Korath III", "Kosmos", "Krelar", "Krikkit", "Krishna", "Kronos", "Krypton", "Kukulkan", "Lagash", "La Maetelle", "Lamarckia", "Lave", "Laythe", "Leesti", "Legis XV", "Leonida", "Leslie", "Londinium", "Luinil", "Lumbar", "Lumen", "Lumiere", "Lusch", "Lusitania", "LV-426", "MacBeth", "Maetel", "Magma", "Magrathea", "Majipoor", "Mare Infinitus", "Marshmalia", "Marune", "Maske", "Maui-Covenant", "Medea", "Meiji", "Mejerr", "Melmac", "Mer", "Meridian", "Merle", "Mesklin", "Metaluna", "Midkemia", "Milokeenia", "Minbar", "Miranda", "Mogo", "Moho", "Mok", "Mondas", "Monea", "Mongo", "Mons", "Mor-Tax", "Morthrai", "Motavia", "Mote Prime", "Mount", "Mustafar", "Naboo", "Nackle", "Nacre", "Namek", "Narn", "Navi", "Nebula 71", "Nebula L-77", "Nebula Z95", "Nede", "Nemesis", "Nenar", "New Amazonia", "New Chicago", "New Earth", "New Namek", "New Terra", "New Vegeta", "Nihil", "Nirn", "Nopalgarth", "Norfolk", "Norion", "Nova Kong", "Nuliajuk", "Nyvan", "Oa", "Oban", "Omicron", "Omnivarium", "Onyx", "Optera", "Ork", "Ormazd", "Orthe", "Osiris", "Pacem", "Palain IX", "Palamok", "Palma", "Palshife", "Pandarve", "Pandora", "Pant", "Parvati", "Peladon", "Pern", "Petaybee", "Phaaze", "Pittsburgh", "Arb", "Plateau", "Plootarg", "Pluto", "Pol", "Pyrrus", "Q-13", "Qar'To", "Qom-Riyadh", "Qo'noS", "Quintessa", "Rain", "Rainbow", "Raxicor", "Reach", "Red Star", "Regis III", "Remulak", "Remus", "Rentus", "Requiem", "Resurgam", "Reverie", "Riedquat", "Rigel", "Rigel 7", "Rime", "River", "Roak", "Roche", "Romulus", "Rougpelt", "Rubanis", "Ruzhena", "Rykros", "Rylos", "Salusa Secundus", "Sanctuary", "Sanghelios", "Sangre", "Saraksh", "Sarris", "Saula", "Sauria", "Sauron", "Second Miltia", "Sedon", "Sedra", "Sergyar", "Serpo", "Sesharrim", "Shadow", "Shaggai", "Shikasta", "Shora", "Sigma Octanus IV", "Signo", "Sihnon", "Skaro", "Sky's Edge", "Solaria", "Solaris", "Solbrecht", "Sol Draconi", "Soror", "Sparta", "Spherus Magna", "Spider", "Spira", "SR-388", "Star One", "St. Ekaterina", "Stroggos", "Styx", "Synnax", "Tagora", "Talark", "Tallon IV", "Tamaran", "Tanis", "Tanith", "Tatooine", "Taurus", "Te", "Tek", "Tellus Secundus", "Tellus Tertius", "Temblor", "Tenebra", "Tergiverse IV", "Terminal", "Terminus", "Thalassa", "Thalassean", "Thallon", "Thel", "Thermia", "The Smoke Ring", "Thra", "Tiamat", "T'ien Shan", "Timbl", "Tirol", "Tissa", "Titan", "Titania", "Tleilax", "Tokyo", "Torto", "Traal", "Tralfamadore", "Trantor", "Trenco", "Tribute", "Trullion", "Tschai", "T'vao", "Twinsun", "Tylo", "U40", "Ummo", "Undomiel", "Unicron", "Uriel", "Urth", "Urtraghus", "Vall", "Vanguard 3", "Vega", "Vegandon", "Vegeta", "Velantia", "Velux", "Venom", "Vindine", "Vladislava", "Vorticon VI", "Vortis", "Vulcan", "Vusstra", "Wallach IX", "Waterloo", "Wegthor", "Wormwood", "Wyst", "X", "X-13", "Xenex", "Xenon", "Xindus", "Yaila", "Yavin 4", "Yellowstone", "Yugopotamia", "Zahir", "Zark", "Zarkon", "Zartron-9", "Zebes", "Zedelbrock 473", "Zedelbrock 475", "Zeelich", "Z'ha'dum", "Zog", "Zok", "Zokk", "Zoness", "Zorg", "Zutinma", "Zyrgon"}} 112 | -------------------------------------------------------------------------------- /content/adventure.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | "github.com/nboughton/go-roll" 10 | ) 11 | 12 | // Adventure represents the elements of an Adventure outline 13 | type Adventure struct { 14 | Seed string 15 | Tag tagFields 16 | } 17 | 18 | type tagFields struct { 19 | Name string 20 | Enemy string 21 | Friend string 22 | Thing string 23 | Place string 24 | Complication string 25 | } 26 | 27 | // NewAdventure throws together a random adventure seed using the table available from Stars Without Number 28 | func NewAdventure(worldTag string) Adventure { 29 | a := Adventure{ 30 | Seed: adventureSeedTable.Roll(), 31 | Tag: tagFields{Name: worldTag}, 32 | } 33 | 34 | t, err := Tags.Find(worldTag) 35 | if err == nil { 36 | a.Tag.Enemy = t.Enemies.Roll() 37 | a.Tag.Friend = t.Friends.Roll() 38 | a.Tag.Thing = t.Things.Roll() 39 | a.Tag.Place = t.Places.Roll() 40 | a.Tag.Complication = t.Complications.Roll() 41 | } 42 | 43 | return a 44 | } 45 | 46 | func (a Adventure) String() string { 47 | buf, str := new(bytes.Buffer), a.Seed 48 | 49 | fmt.Fprintf(buf, "Tag: %s\n", a.Tag.Name) 50 | str = strings.Replace(str, "Enemy", color.RedString("Enemy (%s)", a.Tag.Enemy), -1) 51 | str = strings.Replace(str, "Friend", color.GreenString("Friend (%s)", a.Tag.Friend), -1) 52 | str = strings.Replace(str, "Thing", color.MagentaString("Thing (%s)", a.Tag.Thing), -1) 53 | str = strings.Replace(str, "Place", color.CyanString("Place (%s)", a.Tag.Place), -1) 54 | str = strings.Replace(str, "Complication", color.YellowString("Complication (%s)", a.Tag.Complication), -1) 55 | fmt.Fprintf(buf, "%s", str) 56 | 57 | return buf.String() 58 | } 59 | 60 | // Seed table 61 | var adventureSeedTable = roll.List{ 62 | Items: []string{ 63 | "An Enemy seeks to rob a Friend of some precious Thing that he has desired for some time.", 64 | "A Thing has been discovered on property owned by a Friend, but a Complication risks its destruction.", 65 | "A Complication suddenly hits the party while they’re out doing some innocuous activity.", 66 | "The players unwittingly offend or injure an Enemy, incurring his or her wrath. A Friend offers help in escaping the consequences.", 67 | "Rumor speaks of the discovery of a precious Thing in a distant Place. The players must get to it before an Enemy does.", 68 | "An Enemy has connections with offworld pirates or slavers, and a Friend has been captured by them.", 69 | "A Place has been seized by violent revolutionaries or rebels, and a Friend is being held hostage by them.", 70 | "A Friend is in love with someone forbidden by social convention, and the two of them need help eloping.", 71 | "An Enemy wields tyrannical power over a Friend, relying on the bribery of corrupt local officials to escape consequences.", 72 | "A Friend has been lost in hostile wilderness, and the party must reach a Place to rescue them in the teeth of a dangerous Complication.", 73 | "An Enemy has committed a grave offense against a PC or their family sometime in the past. A Friend shows the party a weakness in the Enemy’s defenses.", 74 | "The party is suddenly caught in a conflict between two warring families or political parties.", 75 | "The party is framed for a crime by an Enemy, and must reach the sanctuary of a Place before they can regroup and find the Thing that will prove their innocence and their Enemy’s perfidy.", 76 | "A Friend is threatened by a tragedy of sickness, legal calamity, or public humiliation, and the only one that seems able to save them is an Enemy.", 77 | "A natural disaster or similar Complication strikes a Place while the party is present, causing great loss of life and property unless the party is able to immediately respond to the injured and trapped.", 78 | "A Friend with a young business has struck a cache of pretech, valuable minerals, or precious salvage. He needs the party to help him reach the Place where the valuables are.", 79 | "An oppressed segment of society starts a sudden revolt in the Place the party is occupying. An Enemy simply lumps the party in with the rebels and tries to put the revolt down with force. A Friend offers them a way to either help the rebels or clear their names.", 80 | "A vulnerable Friend has been targeted for abduction, and has need of guards. A sudden Complication makes guarding them from the Enemy seeking their kidnapping much more difficult. If the Friend is snatched, they must rescue them from a Place.", 81 | "A mysterious Place offers the promise of some precious Thing, but access is very dangerous due to wildlife, hostile locals, or a dangerous environment.", 82 | "An Enemy and a Friend both have legal claim on a Thing, and seek to undermine each other’s case. The Enemy is willing to do murder if he thinks he can get away with it.", 83 | "An Enemy seeks the death of his brother, a Friend, by arranging the failure of his grav flyer or shuttlecraft in dangerous terrain while the party is coincidentally aboard. The party must survive the environment and bring proof of the crime out with them.", 84 | "A Friend seeks to slip word to a lover, one who is also being courted by the Friend’s brother, who is an Enemy. A Complication threatens to cause death or disgrace to the lover unless they either accept the Enemy’s suit or are helped by the party.", 85 | "An Enemy is convinced that one of the party has committed adultery with their flirtatious spouse. He means to lure them to a Place, trap them, and have them killed by the dangers there.", 86 | "An Enemy has been driven insane by exotic recreational drugs or excessive psionic torching. He fixes on a PC as being his mortal nemesis, and plots elaborate deaths, attempting to conceal his involvement amid Complications.", 87 | "A Friend has stolen a precious Thing from an Enemy and fled into a dangerous, inaccessible Place. The party must rescue them, and decide what to do with the Thing and the outraged Enemy.", 88 | "An Enemy has realized that their brother or sister has engaged in a socially unacceptable affair with a Friend, and means to kill both of them unless stopped by the party.", 89 | "A Friend has accidentally caused the death of a family member, and wants the party to help him hide the body or fake an accidental death before his family realizes what has happened. A Complication suddenly makes the task more difficult.", 90 | "A Friend is a follower of a zealous ideologue who plans to make a violent demonstration of the righteousness of his cause, causing a social Complication. The Friend will surely be killed in the aftermath if not rescued or protected by the party.", 91 | "A Friend’s sibling is to be placed in a dangerous situation they’ve got no chance of surviving. The Friend takes their place at the last moment, and will almost certainly die unless the party aids them.", 92 | "Suicide bombers detonate an explosive, chemical, or biological weapon in a Place occupied by the party where a precious Thing is stored The PCs must escape before the Place collapses on top of them, navigating throngs of terrified people in the process and saving the Thing if possible.", 93 | "An Enemy who controls landing permits, oxygen rations, or some other important resource has a prejudice against one or more of the party members. He demands that they bring him a Thing from a dangerous Place before he’ll give them the goods.", 94 | "A Friend in a loveless marriage to an Enemy seeks escape to be with their beloved, and contacts the party to snatch them from their spouse’s guards at a prearranged Place.", 95 | "A Friend seeks to elope with their lover, and contacts the party to help them meet their paramour at a remote, dangerous Place. On arrival, they find that the lover is secretly an Enemy desirous of their removal and merely lured them to the place to meet their doom.", 96 | "The party receives or finds a Thing which proves the crimes of an Enemy yet a Friend was complicit in the crimes, and will be punished as well if the authorities are involved. And the Enemy will stop at nothing to get the item back.", 97 | "A Friend needs to get to a Place on time in order to complete a business contract, but an Enemy means to delay and hinder them until it’s too late, inducing Complications to the trip.", 98 | "A locked pretech stasis pod has been discovered by a Friend, along with directions to the hidden key-code that will open it. The Place where the keycode is hidden is now owned by an Enemy.", 99 | "A fierce schism has broken out in the local majority religion, and an Enemy is making a play to take control of the local hierarchy. A Friend is on the side that will lose badly if the Enemy succeeds, and needs a Thing to prove the other group’s error.", 100 | "A former Enemy has been given reason to repent his treatment of a Friend, and secretly commissions them to help the Friend overcome a Complication. A different Enemy discovers the connection, and tries to paint the PCs as double agents.", 101 | "An alien or a human with extremely peculiar spiritual beliefs seeks to visit a Place for their own reasons. An Enemy of their own kind attempts to stop them before they can reach the Place, and reveal the Thing that was hidden there long ago.", 102 | "A Friend’s sibling is an untrained psychic, and has been secretly using his or her powers to protect the Friend from an Enemy. The neural damage has finally overwhelmed their sanity, and they’ve now kidnapped the Friend to keep them safe. The Enemy is taking this opportunity to make sure the Friend “dies at the hands of their maddened sibling”.", 103 | "A Friend who is a skilled precognitive has just received a flash of an impending atrocity to be committed by an Enemy. He or she needs the party to help them steal the Thing that will prove the Enemy’s plans while dodging the assassins sent to eliminate the precog.", 104 | "A Friend who is an exotic dancer is sought by an Enemy who won’t take no for an answer. The dancer is secretly a Perimeter agent attempting to infiltrate a Place to destroy maltech research, and plots to use the party to help get him or her into the facility under the pretext of striking at the Enemy.", 105 | "A young woman on an interplanetary tour needs the hire of local bodyguards. She turns out to be a trained and powerful combat psychic, but touchingly naive about local dangers, causing a social Complication that threatens to get the whole group arrested.", 106 | "A librarian Friend has discovered an antique databank with the coordinates of a long-lost pretech cache hidden in a Place sacred to a long-vanished religion. The librarian is totally unsuited for danger, but necessary to decipher the obscure religious iconography needed to unlock the cache. The cache is not the anticipated Thing, but something more dangerous to the finder.", 107 | "A fragment of orbital debris clips a shuttle on the way in, and the spaceport is seriously damaged in the crash. The player’s ship or the only vessel capable of getting them off-planet will be destroyed unless the players can organize a response to the dangerous chemical fires and radioactives contaminating the port. A Friend is trapped somewhere in the control tower wreckage.", 108 | "A Friend is allied with a reformist religious group that seeks to break the grip of the current, oppressive hierarchy. The current hierarchs have a great deal of political support with the authorities, but the commoners resent them bitterly. The Friend seeks to secure a remote Place as a meeting-place for the theological rebels.", 109 | "A microscopic black hole punctures an orbital station or starship above the world. Its interaction with the station’s artificial grav generators has thrown everything out of whack, and the station’s become a minefield of dangerously high or zero grav zones. It’s tearing itself apart, and it’s going to collapse soon. An Enemy seeks to escape aboard the last lifeboat and to Hell with everyone else. Meanwhile, a Friend is trying to save his engineer daughter from the radioactive, grav-unstable engine rooms.", 110 | "The planet has a sealed alien ruin, and an Enemy-led cult who worships the vanished builders. They’re convinced that they have the secret to opening and controlling the power inside the ruins, but they’re only half-right. A Friend has found evidence that shows that they’ll only devastate the planet if they meddle with the alien power planet. The party has to get inside the ruins and shut down the engines before it’s too late. Little do they realize that a few aliens survive inside, in a stasis field that will be broken by the ruin’s opening.", 111 | "An Enemy and the group are suddenly trapped in a Place during an accident or Complication. They must work together to escape in time.", 112 | "A telepathic Friend has discovered that an Enemy was responsible for a recent atrocity. Telepathic evidence is useless on this world, however, and if she’s discovered to have scanned his mind she’ll be lobotomized as a ’rogue psychic’. A Thing might be enough to prove his guilt, if the party can figure out how to get to it without revealing their Friend’s meddling.", 113 | "A Friend is responsible for safeguarding a Thing- yet the Thing is suddenly proven to be a fake. The party must find the real object and the Enemy who stole it or else their Friend will be punished as the thief.", 114 | "A Friend is bitten by a poisonous local animal while in a remote Place. The only antidote is back at civilization, yet a Complication threatens to delay the group until it is too late.", 115 | "A lethal plague has started among the residents of the town, but a Complication is keeping aid from reaching them. An Enemy is taking advantage of the panic to hawk a fake cure at ruinous prices, and a Friend is taken in by him. The Complication must be overcome before help can reach the town.", 116 | "A radical political party has started to institute pogroms against “groups hostile to the people”. A Friend is among those groups, and needs to get out of town before an Enemy uses the riot as cover to settle old scores.", 117 | "An Enemy has sold the party an expensive but worthlessly flawed piece of equipment before lighting out for the back country. He and his plunder are holed up at a remote Place.", 118 | "A concert of offworld music is being held in town, and a Friend is slated to be the star performer. Reactionary elements led by an Enemy plot to ruin the “corrupting noise” with sabotage that risks getting performers killed. Meanwhile, a crowd of ignorant offworlder fans have landed and are infuriating the locals.", 119 | "An Enemy is wanted on a neighboring world for some heinous act, and a Friend turns up as a bounty hunter ready to bring him in alive. This world refuses to extradite him, so the capture and retrieval has to evade local law enforcement.", 120 | "An unanticipated solar storm blocks communications and grounds the poorly-shielded grav vehicle that brought the group to this remote Place. Then people start turning up dead; the storm has awoken a dangerous Enemy beast.", 121 | "A Friend has discovered a partially-complete schematic for an ancient pretech refinery unit that produces vast amounts of something precious on this world- water, oxygen, edible compounds, or the like. Several remote Places on the planet are indicated as having the necessary pretech spare parts required to build the device. When finally assembled, embedded self-modification software in the Thing modifies itself into a pretech combat bot. The salvage from it remains very valuable.", 122 | "A Complication ensnares the party where they are in an annoying but seemingly ordinary event. In actuality, an Enemy is using it as cover to strike at a Friend or Thing that happens to be where the PCs are.", 123 | "A Friend has a cranky, temperamental artificial heart installed, and the doctor who put it in is the only one who really understands how it works. The heart has recently started to stutter, but the doctor has vanished. An Enemy has snatched him to fit his elite assassins with very unsafe combat mods.", 124 | "A local clinic is doing wonders in providing free health care to the poor. In truth, it’s a front for an offworld eugenics cult, with useful “specimens” kidnapped and shipped offworld while ’cremated remains’ are given to the family. A Friend is snatched by them, but the party knows they’d have never consented to cremation as the clinic staff claim.", 125 | "Space pirates have cut a deal with an isolated backwoods settlement, off loading their plunder to merchants who meet them there. A Friend goes home to family after a long absence, but is kidnapped or killed before they can bring back word of the dealings. Meanwhile, the party is entrusted with a valuable Thing that must be brought to the ", 126 | "A reclusive psychiatrist is offering treatment for violent mentally ill patients at a remote Place. His treatments seem to work, calming the subjects and returning them to rationality, though major memory loss is involved and some severe social clumsiness ensues. In actuality, he’s removed large portions of their brains to fit them with remote-control units slaved to an AI in his laboratory. He intends to use them as drones to acquire more “subjects”, and eventual control of the town.", 127 | "Vital medical supplies against an impending plague have been shipped in from offworld, but the spike drive craft that was due to deliver them misjumped, and has arrived in-system as a lifeless wreck transmitting a blind distress signal. Whoever gets there first can hold the whole planet hostage, and an Enemy means to do just that.", 128 | "A Friend has spent a substantial portion of their wealth on an ultra-modern new domicile, and invites the party to enjoy a weekend there. An Enemy has hacked the house’s computer system to trap the inhabitants inside and use the automated fittings to kill them.", 129 | "A mud slide, hurricane, earthquake, or other form of disaster strikes a remote settlement. The party is the closest group of responders, and must rescue the locals while dealing with the unearthed, malfunctioning pretech Thing that threatens to cause an even greater calamity if not safely defused.", 130 | "A Friend has found a lost pretech installation, and needs help to loot it. By planetary law, the contents belong to the government.", 131 | "An Enemy mistakes the party for the kind of off-worlders who will murder innocents for pay- assuming they aren’t that kind, at least. He’s sloppy with the contact and unwittingly identifies himself, letting the players know that a Friend will shortly die unless the Enemy is stopped.", 132 | "A party member is identified as a prophesied savior for an oppressed faith or ethnicity. The believers obstinately refuse to believe any protestations to the contrary, and a cynical Enemy in government decides the PC must die simply to prevent the risk of uprising. An equally cynical Friend is determined to push the PC forward as a savior, because that’s what’s needed.", 133 | "Alien beasts escape from a zoo and run wild through the spectators. The panicked owner offers large rewards for recapturing them live, but some of the beasts are quite dangerous.", 134 | "A trained psychic is accused of going feral by an Enemy. The psychic had already suffered severe neural damage before being found for training, so brain scans cannot establish physical signs of madness. The psychic seems unstable, but not violent- at least, on short acquaintance. The psychic offers a psychic PC the secrets of a unique psychic technique if they help him flee.", 135 | "A Thing is the token of rulership on this world, and it’s gone missing. If it’s not found rapidly, the existing ruler will be deposed. Evidence left at a Place suggests that an Enemy has it, but extralegal means are necessary to investigate fully.", 136 | "Psychics are vanishing, including a Friend. They’re being kidnapped by an ostensibly-rogue government researcher who is using them to research the lost psychic disciplines that helped enable pretech manufacturing, and they are being held at a remote Place. The snatcher is a small-time local Enemy with unnaturally ample resources.", 137 | "A Friend desperately seeks to hide evidence of some past crime that will ruin his life should it come to light. An Enemy holds the Thing that proves his involvement, and blackmails him ruthlessly.", 138 | "A courier mistakes the party for the wrong set of offworlders, and wordlessly deposits a Thing with them that implies something awful- med-frozen, child-sized human organs, for example, or a private catalog of gengineered human slaves. The courier’s boss shortly realizes the error, and this Enemy tries to silence the PCs while preserving the Place where his evil is enacted.", 139 | "A slowboat system freighter is taken over by Enemy separatist terrorists at the same time as the planet’s space defenses are taken offline by internal terrorist attacks. The freighter is aimed straight at the starport, and will crash into it in hours if not stopped.", 140 | "Alien artifacts on the planet’s surface start beaming signals into the system’s asteroid belt. The signals provoke a social Complication in panicked response, and an Enemy seeks to use the confusion to take over. The actual effect of the signals might be harmless, or might summon a long-lost alien AI warship to scourge life from the world.", 141 | "An alien ambassador Friend is targeted by xeno-phobe Enemy assassins. Relations are so fragile that if the ambassador even realizes that humans are making a serious effort to kill him, the result may be war.", 142 | "A new religion is being preached by a Friend on this planet. Existing faiths are not amused, and an Enemy among the hierarchy is provoking the people to persecute the new believers, hoping for things to get out of hand.", 143 | "An Enemy was once the patron of a Friend until the latter was betrayed. Now the Friend wants revenge, and they think they have the information necessary to get past the Enemy’s defenses.", 144 | "Vital life support or medical equipment has been sabotaged by offworlders or zealots, and must be repaired before time runs out. The only possible source of parts is at a Place, and the saboteurs can be expected to be working hard to get there and destroy them, too.", 145 | "A Friend is importing offworld tech that threatens to completely replace the offerings of an Enemy businessman. The Enemy seeks to sabotage the friend’s stock, and thus ’prove’ its inferiority.", 146 | "An Exchange diplomat is negotiating for the opening of a branch of the interstellar bank on this world. An Enemy among the local banks wants to show the world as being ungovernably unstable, so provokes Complications and riots around the diplomat.", 147 | "An Enemy is infuriated by the uppity presumption of an ambitious Friend of a lower social caste, and tries to pin a local Complication on the results of his unnatural rejection of his proper place.", 148 | "A Friend is working for an offworld corporation to open a manufactory, and is ignoring local traditions that privilege certain social or ethnic groups, giving jobs to the most qualified workers instead. An angry Enemy seeks to sabotage the factory.", 149 | "An offworld musician who was revered as little less than a god on his homeworld requires bodyguards. He immediately acquires Enemies on this world with his riotous ways, and his guards must keep him from getting arrested if they are to be paid.", 150 | "Atmospheric disturbances, dust storms, or other particulate clouds suddenly blow into town, leaving the settlement blind. An Enemy commits a murder during the darkness, and attempts to frame the players as convenient scapegoats.", 151 | "An Enemy spikes the oxygen supply of an orbital station or unbreathable-atmosphere hab dome with hallucinogens as cover for a theft. Most victims are merely confused and disoriented, but some become violent in their delusions. By chance, the party’s air supply was not contaminated.", 152 | "By coincidence, one of the party members is wearing clothing indicative of membership in a violent political group, and thus the party is treated in friendly fashion by a local Enemy for no obvious reason. The Enemy assumes that the party will go along with some vicious crime without complaint, and the group isn’t informed of what’s in the offing until they’re in deep.", 153 | "A local ruler wishes outworlders to advise him of the quality of his execrable poetry- and is the sort to react very poorly to anything less that evidently sincere and fulsome praise. Failure to amuse the ruler results in the party being dumped in a dangerous Place to “experience truly poetic solitude”.", 154 | "A Friend among the locals is unreasonably convinced that offworlder tech can repair anything, and has blithely promised a powerful local Enemy that the party can easily fix a damaged pretech Thing. The Enemy has invested in many expensive spare parts, but the truly necessary pieces are kept in a still-dangerous pretech installation in a remote Place.", 155 | "The party’s offworld comm gear picks up a chance transmission from the local government and automatically descrambles the primitive encryption key. The document is proof that an Enemy in government intends to commit an atrocity against a local village with a group of “deniable” renegades in order to steal a Thing kept in the village.", 156 | "A Friend belongs to a persecuted faith, ethnicity, or social class, and appeals for the PCs to help a cell of rebels get offworld before the Enemy law enforcement finds them.", 157 | "A part on the party’s ship or the only other transport out has failed, and needs immediate replacement. The only available part is held by an Enemy, who will only willingly relinquish it in exchange for a Thing held by an innocent Friend who will refuse to sell at any price.", 158 | "Eugenics cultists are making gengineered slaves out of genetic material gathered at a local brothel. Some of the unnaturally tempting slaves are being slipped among the prostitutes as bait to infatuate powerful officials, while others are being sold under the table to less scrupulous elites.", 159 | "Evidence has been unearthed at a Place that substantial portions of the planet are actually owned by members of an oppressed and persecuted group. The local courts have no intention of recognizing the rights, but the codes with the ownership evidence would allow someone to bypass a number of antique pretech defenses around the planetary governor’s palace. A Friend wants the codes to pass to his friends among the group’s rebels.", 160 | "A crop smut threatens the planet’s agriculture, promising large-scale famine. A Friend finds evidence that a secret government research station in the system’s asteroid belt was conducting experiments in disease-resistant crop strains for the planet before the Silence struck and cut off communication with the station. The existing government considers it a wild goose chase, but the party might choose to help. The station has stasis-frozen samples of the crop sufficient to avert the famine, but it also has less pleasant relics….", 161 | "A grasping Enemy in local government seizes the party’s ship for some trifling offense. The Enemy wants to end offworld trade, and is trying to scare other traders away. The starship is held within a military cordon, and the Enemy is confident that by the time other elements of the government countermand the order, the free traders will have been spooked off.", 162 | "A seemingly useless trinket purchased by a PC turns out to be the security key to a lost pretech facility. It was sold by accident by a bungling and now-dead minion of a local Enemy, who is hot after the party to “reclaim” his property… preferably after the party defeats whatever automatic defenses and bots the facility might still support.", 163 | }, 164 | } 165 | --------------------------------------------------------------------------------