├── config.json ├── Makefile ├── .gitignore ├── welcome.txt ├── make-packages.sh ├── internal ├── world.go ├── user.go ├── keymaker.go ├── creatures.go ├── placenames.go ├── items.go ├── input.go ├── terrain.go ├── sshserver.go ├── battle.go ├── usersetup.go ├── worldbuilder.go ├── classsystem.go ├── util.go ├── screen.go └── terraingeneration.go ├── go.mod ├── cmd ├── mud-server │ └── main.go └── mud-ui │ └── main.go ├── LICENSE ├── items.json ├── README.md ├── go.sum ├── bestiary.json └── terrain.json /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Listen": ":2222" 3 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUTFILES := $(patsubst cmd/%/main.go,bin/%,$(wildcard cmd/*/main.go)) 2 | 3 | bin/%: cmd/%/main.go 4 | go build -o $@ $< 5 | 6 | all: clean mod $(OUTFILES) 7 | 8 | mod: 9 | go mod download 10 | 11 | clean: 12 | rm bin/* || true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | id_rsa 7 | /bin/ 8 | vendor/ 9 | *.db 10 | *.zip 11 | *.tgz 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 20 | .glide/ 21 | 22 | # IDE 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /welcome.txt: -------------------------------------------------------------------------------- 1 | ^Welcome! 2 | This is a simple graphical MUD developed for the 3 | Enter The Mud code jam on itch.io. 4 | 5 | ^Map 6 | ≡ means stuff is on the ground 7 | ∆ means bad guys are hanging out 8 | ≜ means there are bad guys and stuff 9 | * is a player, middle one is you 10 | 11 | ^Controls 12 | %Cursor keys: Move 13 | %/: Open command mode (nonfunctional) 14 | %!: Open chat mode 15 | %Esc: Toggle sticky chat 16 | %Ctrl-C: Quit 17 | %Tab: Toggle log/inventory panel view 18 | 19 | Other keys are labelled in the UI. 20 | 21 | ^❦ -------------------------------------------------------------------------------- /make-packages.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | OSSES="darwin linux windows" 4 | ARCHES="amd64" 5 | ZIPPREFIX=`date +%Y-%m-%d` 6 | 7 | for OS in $OSSES 8 | do 9 | for ARCH in $ARCHES 10 | do 11 | echo app-$OS-$ARCH 12 | OUTDIRNAME=mud-$OS-$ARCH 13 | mkdir $OUTDIRNAME 14 | ENABLE_CGO="" 15 | 16 | INFILE=cmd/mud.go 17 | OUTITEM=$OUTDIRNAME/mud 18 | if [ "z$OS" = "zlinux" ] 19 | then 20 | echo "Linux" 21 | elif [ "z$OS" = "zwindows" ] 22 | then 23 | OUTITEM=$OUTITEM.exe 24 | elif [ "z$OS" = "zdarwin" ] 25 | then 26 | INFILE=cmd/mud-ui.go 27 | fi 28 | 29 | # brew install mingw-w64 30 | GOOS=$OS GOARCH=$ARCH go build -o $OUTITEM $INFILE 31 | 32 | cp *.json *.txt $OUTDIRNAME 33 | if [ "z$OS" = "zwindows" ] 34 | then 35 | zip mud-$ZIPPREFIX-$OS-$ARCH.zip $OUTDIRNAME/* 36 | else 37 | tar cvzf mud-$ZIPPREFIX-$OS-$ARCH.tgz $OUTDIRNAME 38 | fi 39 | rm -rf $OUTDIRNAME 40 | done 41 | done 42 | -------------------------------------------------------------------------------- /internal/world.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | // Cell represents the data about a living cell 4 | type Cell interface { 5 | Location() Point 6 | IsEmpty() bool 7 | CellInfo() *CellInfo 8 | SetCellInfo(*CellInfo) 9 | 10 | GetCreatures() []*Creature 11 | HasCreatures() bool 12 | UpdateCreature(*Creature) 13 | ClearCreatures() 14 | AddStockCreature(string) 15 | 16 | InventoryItems() []*InventoryItem 17 | AddInventoryItem(*InventoryItem) bool 18 | InventoryItem(string) *InventoryItem 19 | PullInventoryItem(string) *InventoryItem 20 | HasInventoryItems() bool 21 | } 22 | 23 | // World represents a gameplay world. It should keep track of the map, 24 | // entities in the map, and players. 25 | type World interface { 26 | GetDimensions() (uint32, uint32) 27 | GetUser(string) User 28 | 29 | Cell(uint32, uint32) Cell 30 | CellAtPoint(Point) Cell 31 | KillCreature(string) 32 | Attack(interface{}, interface{}, *Attack) 33 | 34 | NewPlaceID() uint64 35 | OnlineUsers() []User 36 | Chat(LogItem) 37 | Close() 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mud 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ahmetb/go-cursor v0.0.0-20131010032410-8136607ea412 7 | github.com/andlabs/ui v0.0.0-20200610043537-70a69d6ae31e 8 | github.com/gliderlabs/ssh v0.3.4 9 | github.com/google/uuid v1.3.0 10 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d 11 | github.com/ojrac/opensimplex-go v1.0.2 12 | github.com/vmihailenco/msgpack v4.0.4+incompatible 13 | go.etcd.io/bbolt v1.3.6 14 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d 15 | ) 16 | 17 | require ( 18 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 19 | github.com/golang/protobuf v1.3.1 // indirect 20 | github.com/mattn/go-colorable v0.1.12 // indirect 21 | github.com/mattn/go-isatty v0.0.14 // indirect 22 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 23 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect 24 | google.golang.org/appengine v1.6.7 // indirect 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /cmd/mud-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | 10 | mud "mud/internal" 11 | ) 12 | 13 | var configFile = "./config.json" 14 | 15 | type serverconfig struct { 16 | Listen string `json:""` 17 | } 18 | 19 | func loadConfig(config *serverconfig) { 20 | data, err := ioutil.ReadFile(configFile) 21 | 22 | if err == nil { 23 | err = json.Unmarshal(data, config) 24 | } 25 | 26 | if err != nil { 27 | log.Printf("Error parsing %s: %v", configFile, err) 28 | } 29 | } 30 | 31 | func main() { 32 | log.Println("Starting") 33 | executable, err := os.Executable() 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | if _, err := os.Stat(configFile); err != nil { 39 | executablePath, err := filepath.Abs(filepath.Dir(executable)) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | log.Printf("Going to folder %v...", executablePath) 45 | 46 | os.Chdir(executablePath) 47 | } 48 | 49 | var config serverconfig 50 | 51 | loadConfig(&config) 52 | 53 | mud.LoadResources() 54 | mud.ServeSSH(config.Listen) 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jason Scheirer 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/mud-ui/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/andlabs/ui" 12 | 13 | mud "mud/internal" 14 | ) 15 | 16 | var configFile = "./config.json" 17 | 18 | type serverconfig struct { 19 | Listen string `json:""` 20 | } 21 | 22 | func loadConfig(config *serverconfig) { 23 | data, err := ioutil.ReadFile(configFile) 24 | 25 | if err == nil { 26 | err = json.Unmarshal(data, config) 27 | } 28 | 29 | if err != nil { 30 | log.Printf("Error parsing %s: %v", configFile, err) 31 | } 32 | } 33 | 34 | func main() { 35 | log.Println("Starting") 36 | executable, err := os.Executable() 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | if _, err := os.Stat(configFile); err != nil { 42 | executablePath, err := filepath.Abs(filepath.Dir(executable)) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | log.Printf("Going to folder %v...", executablePath) 48 | 49 | os.Chdir(executablePath) 50 | } 51 | 52 | var config serverconfig 53 | loadConfig(&config) 54 | mud.LoadResources() 55 | go mud.ServeSSH(config.Listen) 56 | 57 | uierr := ui.Main(func() { 58 | box := ui.NewVerticalBox() 59 | box.SetPadded(true) 60 | box.Append(ui.NewLabel(fmt.Sprintf("Running SSH server on %v", config.Listen)), false) 61 | window := ui.NewWindow("MUD SSH Server", 400, 50, false) 62 | window.SetChild(box) 63 | window.OnClosing(func(*ui.Window) bool { 64 | ui.Quit() 65 | return true 66 | }) 67 | window.Show() 68 | }) 69 | if uierr != nil { 70 | panic(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/user.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | // SlottedInventoryItem describe the slots and items in the slots 4 | type SlottedInventoryItem struct { 5 | Name string 6 | Item *InventoryItem 7 | } 8 | 9 | // EquipUserInfo is for putting outfits on a user 10 | type EquipUserInfo interface { 11 | Equip(string, *InventoryItem) (*InventoryItem, error) 12 | EquippableSlots(*InventoryItem) []string 13 | CanEquip(string, *InventoryItem) bool 14 | Equipped() []SlottedInventoryItem 15 | EquipmentSlotItem(string) *InventoryItem 16 | EquipSlots() []string 17 | } 18 | 19 | // EquipmentSlotInfo Decribes a user's open equipment slots 20 | type EquipmentSlotInfo struct { 21 | Name string `json:""` 22 | SlotTypes []string `json:""` 23 | } 24 | 25 | // User represents an active user in the system. 26 | type User interface { 27 | StatInfo 28 | StatPointable 29 | FullStatPointable 30 | ClassInfo 31 | LastAction 32 | ChargeInfo 33 | InventoryInfo 34 | EquipUserInfo 35 | 36 | Username() string 37 | Title() string 38 | IsInitialized() bool 39 | Initialize(bool) 40 | Location() *Point 41 | 42 | MoveNorth() 43 | MoveSouth() 44 | MoveEast() 45 | MoveWest() 46 | ChargePoints() 47 | 48 | Log(message LogItem) 49 | GetLog() []LogItem 50 | 51 | MarkActive() 52 | Cell() Cell 53 | LocationName() string 54 | 55 | Respawn() 56 | Reload() 57 | Save() 58 | } 59 | 60 | // LastAction tracks the last time an actor performed an action, for charging action bar. 61 | type LastAction interface { 62 | Act() 63 | GetLastAction() int64 64 | } 65 | 66 | // ChargeInfo returns turn-base charge time info 67 | type ChargeInfo interface { 68 | Charge() (int64, int64) 69 | Attacks() []*AttackInfo 70 | MusterAttack(string) *Attack 71 | MusterCounterAttack() *Attack 72 | } 73 | 74 | // UserSSHAuthentication for storing SSH auth. 75 | type UserSSHAuthentication interface { 76 | SSHKeysEmpty() bool 77 | ValidateSSHKey(string) bool 78 | AddSSHKey(string) 79 | } 80 | -------------------------------------------------------------------------------- /internal/keymaker.go: -------------------------------------------------------------------------------- 1 | // Borrowed from https://gist.github.com/devinodaniel/8f9b8a4f31573f428f29ec0e884e6673 2 | 3 | package mud 4 | 5 | import ( 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | ) 14 | 15 | func makeKeyFiles() string { 16 | savePrivateFileTo := "./id_rsa" 17 | 18 | _, err := os.Stat(savePrivateFileTo) 19 | if err == nil { 20 | log.Printf("Key file %s already exists", savePrivateFileTo) 21 | return savePrivateFileTo 22 | } 23 | 24 | log.Printf("Generating %s...", savePrivateFileTo) 25 | 26 | bitSize := 4096 27 | 28 | privateKey, err := generatePrivateKey(bitSize) 29 | if err != nil { 30 | log.Fatal(err.Error()) 31 | } 32 | 33 | privateKeyBytes := encodePrivateKeyToPEM(privateKey) 34 | 35 | err = writeKeyToFile(privateKeyBytes, savePrivateFileTo) 36 | if err != nil { 37 | log.Fatal(err.Error()) 38 | } 39 | 40 | return savePrivateFileTo 41 | } 42 | 43 | // generatePrivateKey creates a RSA Private Key of specified byte size 44 | func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) { 45 | // Private Key generation 46 | privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // Validate Private Key 52 | err = privateKey.Validate() 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | log.Println("Private Key generated") 58 | return privateKey, nil 59 | } 60 | 61 | // encodePrivateKeyToPEM encodes Private Key from RSA to PEM format 62 | func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { 63 | // Get ASN.1 DER format 64 | privDER := x509.MarshalPKCS1PrivateKey(privateKey) 65 | 66 | // pem.Block 67 | privBlock := pem.Block{ 68 | Type: "RSA PRIVATE KEY", 69 | Headers: nil, 70 | Bytes: privDER, 71 | } 72 | 73 | // Private key in PEM format 74 | privatePEM := pem.EncodeToMemory(&privBlock) 75 | 76 | return privatePEM 77 | } 78 | 79 | // writePemToFile writes keys to a file 80 | func writeKeyToFile(keyBytes []byte, saveFileTo string) error { 81 | err := ioutil.WriteFile(saveFileTo, keyBytes, 0600) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | log.Printf("Key saved to: %s", saveFileTo) 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/creatures.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | ) 8 | 9 | // CreatureTypes is a mapping of string IDs to creature types 10 | var CreatureTypes map[string]CreatureType 11 | 12 | // CreatureSpawn is a JSON struct used for the generation of monsters 13 | type CreatureSpawn struct { 14 | Name string `json:""` // ID of creature in bestiary 15 | Probability float32 `json:""` // 0-1.0 16 | Cluster float32 `json:""` // 0-1.0 17 | } 18 | 19 | // CreatureType is the type of creature (Hostile: true is monster, false is NPC) 20 | type CreatureType struct { 21 | ID string `json:"-"` 22 | Name string `json:""` 23 | Hostile bool `json:""` 24 | MaxHP uint64 `json:""` 25 | MaxMP uint64 `json:""` 26 | MaxAP uint64 `json:""` 27 | MaxRP uint64 `json:""` 28 | Attacks []Attack `json:""` 29 | ItemDrops []ItemDrop `json:""` // List of items and probabilities of them appearing in each terrain type 30 | } 31 | 32 | // Creature is an instance of a Creature 33 | type Creature struct { 34 | ID string `json:""` 35 | CreatureType string `json:""` 36 | X uint32 `json:""` 37 | Y uint32 `json:""` 38 | HP uint64 `json:""` 39 | AP uint64 `json:""` 40 | RP uint64 `json:""` 41 | MP uint64 `json:""` 42 | CreatureTypeStruct CreatureType `json:"-"` 43 | Charge int64 `json:"-"` 44 | maxCharge int64 45 | world World 46 | } 47 | 48 | // StatPoints is for StatPointable 49 | func (creature *Creature) StatPoints() StatPoints { 50 | return StatPoints{ 51 | AP: creature.CreatureTypeStruct.MaxAP, 52 | RP: creature.CreatureTypeStruct.MaxRP, 53 | MP: creature.CreatureTypeStruct.MaxMP} 54 | } 55 | 56 | // FullStatPoints gets a fullstatinfo object for battle calculation arithmetic 57 | func (creature *Creature) FullStatPoints() FullStatPoints { 58 | return FullStatPoints{ 59 | StatPoints: creature.StatPoints(), 60 | HP: creature.HP, 61 | Trample: 0} 62 | } 63 | 64 | // CreatureList represents the creatures in a DB 65 | type CreatureList struct { 66 | CreatureIDs []string `json:""` 67 | } 68 | 69 | func loadCreatureTypes(creatureInfoFile string) { 70 | data, err := ioutil.ReadFile(creatureInfoFile) 71 | 72 | if err == nil { 73 | err = json.Unmarshal(data, &CreatureTypes) 74 | } 75 | 76 | for k, v := range CreatureTypes { 77 | v.ID = k 78 | CreatureTypes[k] = v 79 | } 80 | 81 | if err != nil { 82 | log.Printf("Error parsing %s: %v", creatureInfoFile, err) 83 | } 84 | } 85 | 86 | func init() { 87 | CreatureTypes = make(map[string]CreatureType) 88 | } 89 | -------------------------------------------------------------------------------- /items.json: -------------------------------------------------------------------------------- 1 | { 2 | "Simple Sword": { 3 | "Type": "Weapon", 4 | "Subtype": "Sword", 5 | "Description": "The most basic melee weapon imaginable", 6 | "Attacks": [ 7 | { 8 | "Name": "Slice", 9 | "Accuracy": 70, 10 | "MP": 0, 11 | "AP": 2, 12 | "RP": 0, 13 | "Trample": 4, 14 | "Charge": 1, 15 | "Bonuses": "AP+25%AP;TP+10%AP" 16 | }, 17 | { 18 | "Name": "Hack", 19 | "Accuracy": 80, 20 | "MP": 0, 21 | "AP": 8, 22 | "RP": 0, 23 | "Trample": 12, 24 | "Charge": 3, 25 | "Bonuses": "AP+50%AP;TP+10%MP" 26 | } 27 | ] 28 | }, 29 | "Simple Bow": { 30 | "Type": "Weapon", 31 | "Subtype": "Bow", 32 | "Description": "The most basic ranged weapon imaginable", 33 | "Attacks": [ 34 | { 35 | "Name": "Quick Fire", 36 | "Accuracy": 70, 37 | "MP": 0, 38 | "AP": 0, 39 | "RP": 2, 40 | "Trample": 4, 41 | "Charge": 1, 42 | "Bonuses": "RP+25%RP;TP+10%RP" 43 | }, 44 | { 45 | "Name": "Arrow", 46 | "Accuracy": 80, 47 | "MP": 0, 48 | "AP": 0, 49 | "RP": 8, 50 | "Trample": 4, 51 | "Charge": 3, 52 | "Bonuses": "RP+50%AP;TP+10%RP" 53 | } 54 | ] 55 | }, 56 | "Simple Wand": { 57 | "Description": "The most basic magical weapon imaginable", 58 | "Type": "Weapon", 59 | "Subtype": "Wand", 60 | "Attacks": [ 61 | { 62 | "Name": "Spontaneous Spark", 63 | "Accuracy": 70, 64 | "MP": 1, 65 | "AP": 0, 66 | "RP": 0, 67 | "Trample": 4, 68 | "Charge": 1, 69 | "Bonuses": "MP+25%MP;TP+10%MP" 70 | }, 71 | { 72 | "Name": "Blast", 73 | "Accuracy": 80, 74 | "MP": 8, 75 | "AP": 0, 76 | "RP": 0, 77 | "Trample": 4, 78 | "Charge": 3, 79 | "Bonuses": "MP+50%AP;TP+10%MP" 80 | } 81 | ] 82 | }, 83 | "Shiny Rock": { 84 | "Description": "You've spent too long in the grasses if you think this is interesting", 85 | "Type": "Artifact", 86 | "Subtype": "Curiosity" 87 | }, 88 | "Broken Rock": { 89 | "Description": "Someone did something crazy with this rock", 90 | "Type": "Artifact", 91 | "Subtype": "Curiosity" 92 | }, 93 | "Skull": { 94 | "Description": "Someone had this inside their head once", 95 | "Type": "Artifact", 96 | "Subtype": "Curiosity" 97 | } 98 | } -------------------------------------------------------------------------------- /internal/placenames.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | ) 7 | 8 | var onsets, vowels, nucleae, codae, prefixes, middles, suffixes []string 9 | 10 | func randomOnset() string { 11 | if rand.Int()%2 == 0 { 12 | return randomVowel() 13 | } 14 | return onsets[rand.Int()%len(onsets)] 15 | } 16 | 17 | func randomNucleus() string { 18 | return nucleae[rand.Int()%len(nucleae)] 19 | } 20 | 21 | func randomVowel() string { 22 | return vowels[rand.Int()%len(vowels)] 23 | } 24 | 25 | func randomCoda() string { 26 | return codae[rand.Int()%len(codae)] 27 | } 28 | 29 | func randomRhyme(inWord bool) string { 30 | if inWord && rand.Int()%4 == 0 { 31 | return randomNucleus() 32 | } else if rand.Int()%4 == 0 { 33 | return randomVowel() + randomCoda() + randomVowel() 34 | } 35 | return randomVowel() + randomCoda() 36 | } 37 | 38 | func randomName() string { 39 | return prefixes[rand.Int()%len(prefixes)] + middles[rand.Int()%len(middles)] + suffixes[rand.Int()%len(suffixes)] 40 | } 41 | 42 | // RandomPlaceName generates a random place name 43 | func randomPlaceName() string { 44 | name := "" 45 | for w := 0; w < 1+rand.Int()%2; w++ { 46 | if len(name) > 0 { 47 | name += " " 48 | } 49 | if rand.Int()%2 == 0 { 50 | noPrefix := true 51 | if rand.Int()%2 == 0 { 52 | noPrefix = false 53 | name += prefixes[rand.Int()%len(prefixes)] 54 | } 55 | for i := 0; i < 1+rand.Int()%2; i++ { 56 | name += randomOnset() + randomRhyme(i > 0) 57 | } 58 | if rand.Int()%2 == 0 || noPrefix { 59 | name += suffixes[rand.Int()%len(suffixes)] 60 | } 61 | } else { 62 | name += randomName() 63 | } 64 | } 65 | 66 | if len(name) > 25 { 67 | return randomPlaceName() 68 | } 69 | 70 | return strings.Title(name) 71 | } 72 | 73 | func init() { 74 | onsets = []string{"s", "sp", "spr", "spl", "th", "z", "g", "gr", "n", "m"} 75 | nucleae = []string{"en", "em", "ul", "er", "il", "po", "to"} 76 | vowels = []string{"a", "i", "u", "e", "o"} 77 | codae = []string{"p", "t", "k", "f", "s", "sh", "os", "ers", ""} 78 | prefixes = []string{"nor", "sur", "wess", "ess", "jer", "hamp", "penrhyn", "trans", "mid", "man", "men", "sir", "dun", "beas", "newydd", "pant", "new ", "old ", "den", "high", "ast", "black", "white", "green", "castle", "heck", "hell", "button", "glen", "myr", "griffin", "lion", "bear", "pegasus", "sheep", "goat", "grouse", "pelican", "gull", "sparrow", "hawks", "starling", "badger", "otter", "tiger", "goose", "hogs", "hedgehog", "mouse", "shields", "swords", "spears", "cloaks", "gloven", "circus", "corn", "gren"} 79 | middles = []string{"helms", "al", "ox", "horse", "tree", "sylvania", "stone", "men", "fond", "muck", "cross", "snake", "yank", "her", "dam", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""} 80 | suffixes = []string{"fill", "sley", "sey", "spey", "well", "stone", "wich", "ddych", "thorpe", "den", "ton", "chester", "worth", "land", "hole", "park", "ware", "ine", "pile", "ina", "feld", "hoff", "wind", "dal", "hope", "kirk", "cen", "eux", "ans", "mont", "noble", "hole", "corner", "bend", "place", "mawr", "circle", "square", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""} 81 | } 82 | -------------------------------------------------------------------------------- /internal/items.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | ) 8 | 9 | // ItemTypes is a mapping of string item names to item types 10 | var ItemTypes map[string]InventoryItem 11 | 12 | // ItemDrop is a JSON struct used for the generation of random drops 13 | type ItemDrop struct { 14 | Name string `json:""` // Name of item in items.json 15 | Cluster uint `json:""` // 0-10000 16 | Probability float32 `json:""` // 0-1.0 17 | } 18 | 19 | // For the Type field of Item 20 | const ( 21 | ITEMTYPEWEAPON = "Weapon" 22 | ITEMTYPEPOTION = "Potion" 23 | ITEMTYPESCROLL = "Scroll" 24 | ITEMTYPEARMOR = "Armor" 25 | ITEMTYPEARTIFACT = "Artifact" 26 | ) 27 | 28 | // Weapon types 29 | const ( 30 | WEAPONSUBTYPESWORD = "Sword" // Melee 31 | WEAPONSUBTYPESPEAR = "Spear" // Melee/Range 32 | WEAPONSUBTYPEDAGGER = "Dagger" // Melee/Magic 33 | WEAPONSUBTYPEBOW = "Bow" // Range 34 | WEAPONSUBTYPEDART = "Dart" // Range/Magic 35 | WEAPONSUBTYPEJAVELIN = "Javelin" // Range/Melee 36 | WEAPONSUBTYPEWAND = "Wand" // Magic 37 | WEAPONSUBTYPESTAFF = "Staff" // Magic/Melee 38 | WEAPONSUBTYPEORB = "Orb" // Magic/Range 39 | ) 40 | 41 | // Armor Types 42 | const ( 43 | ARMORSUBTYPEHELM = "Helmet" 44 | ARMORSUBTYPEHAT = "Hat" 45 | ARMORSUBTYPECOWL = "Cowl" 46 | ARMORSUBTYPECHESTPLATE = "Chestplate" 47 | ARMORSUBTYPELIGHTARMOR = "Light Armor" 48 | ARMORSUBTYPECLOAK = "Cloak" 49 | ARMORSUBTYPEGAUNTLET = "Gauntlet" 50 | ARMORSUBTYPEBRACERS = "Bracers" 51 | ARMORSUBTYPEGLOVES = "Gloves" 52 | ARMORSUBTYPESHIELD = "Shield" 53 | ) 54 | 55 | // Artifact types 56 | const ( 57 | ARTIFACTTYPEAMULET = "Amulet" 58 | ARTIFACTTYPERELIC = "Relic" 59 | ARTIFACTTYPECURIOSITY = "Curiosity" 60 | ARTIFACTTYPEINGREDIENT = "Ingredient" 61 | ) 62 | 63 | // InventoryItem is a droppable item for an inventory 64 | type InventoryItem struct { 65 | ID string `json:""` 66 | Name string `json:""` 67 | Type string `json:""` 68 | Description string `json:""` 69 | Subtype string `json:",omitempty"` // For weapons and artifacts 70 | Attacks []Attack `json:",omitempty"` // For weapons and spells 71 | CounterAttacks []Attack `json:",omitempty"` // For scrolls and spells with counterattack effects 72 | } 73 | 74 | // SlotName is the places a potential item can be equipped 75 | func (item *InventoryItem) SlotName() string { 76 | if len(item.Subtype) > 0 { 77 | return item.Subtype 78 | } 79 | 80 | return item.Type 81 | } 82 | 83 | // InventoryInfo handles a thing with inventory 84 | type InventoryInfo interface { 85 | InventoryItems() []*InventoryItem 86 | AddInventoryItem(*InventoryItem) bool 87 | InventoryItem(string) *InventoryItem 88 | PullInventoryItem(string) *InventoryItem 89 | } 90 | 91 | func loadItemTypes(itemInfoFile string) { 92 | data, err := ioutil.ReadFile(itemInfoFile) 93 | 94 | if err == nil { 95 | err = json.Unmarshal(data, &ItemTypes) 96 | } 97 | 98 | for k, v := range ItemTypes { 99 | v.Name = k 100 | ItemTypes[k] = v 101 | } 102 | 103 | if err != nil { 104 | log.Printf("Error parsing %s: %v", itemInfoFile, err) 105 | } 106 | } 107 | 108 | func init() { 109 | ItemTypes = make(map[string]InventoryItem) 110 | } 111 | -------------------------------------------------------------------------------- /internal/input.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "strconv" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type inputEvent struct { 13 | inputString string 14 | position Point 15 | err error 16 | } 17 | 18 | type inputState int 19 | 20 | const ( 21 | stateOUTOFSEQUENCE inputState = iota 22 | stateINESCAPE 23 | stateDIRECTIVE 24 | stateQUESTION 25 | ) 26 | 27 | // sleepThenReport is a timeout sequence so that if the escape key is pressed it will register 28 | // as a keypress within a reasonable period of time with the input loop, even if the input 29 | // state machine is in its "inside ESCAPE press listening for extended sequence" state. 30 | func sleepThenReport(stringChannel chan<- inputEvent, escapeCanceller *sync.Once, state *inputState) { 31 | time.Sleep(50 * time.Millisecond) 32 | 33 | escapeCanceller.Do(func() { 34 | *state = stateOUTOFSEQUENCE 35 | stringChannel <- inputEvent{"ESCAPE", Point{}, nil} 36 | }) 37 | } 38 | 39 | func handleKeys(reader *bufio.Reader, stringChannel chan<- inputEvent, cancel context.CancelFunc) { 40 | inputGone := errors.New("Input ended") 41 | inEscapeSequence := stateOUTOFSEQUENCE 42 | var escapeCanceller *sync.Once 43 | emptyPoint := Point{} 44 | 45 | codeMap := map[rune]string{ 46 | rune(9): "TAB", 47 | rune(13): "ENTER", 48 | rune(127): "BACKSPACE", 49 | } 50 | 51 | for { 52 | runeRead, _, err := reader.ReadRune() 53 | 54 | if err != nil || runeRead == 3 { 55 | stringChannel <- inputEvent{"", emptyPoint, inputGone} 56 | cancel() 57 | return 58 | } 59 | 60 | if escapeCanceller != nil { 61 | escapeCanceller.Do(func() { escapeCanceller = nil }) 62 | } 63 | 64 | if inEscapeSequence == stateOUTOFSEQUENCE && runeRead == 27 { 65 | inEscapeSequence = stateINESCAPE 66 | escapeCanceller = new(sync.Once) 67 | go sleepThenReport(stringChannel, escapeCanceller, &inEscapeSequence) 68 | } else if inEscapeSequence == stateINESCAPE { 69 | if string(runeRead) == "[" { 70 | inEscapeSequence = stateDIRECTIVE 71 | } else if runeRead == 27 { 72 | stringChannel <- inputEvent{"ESCAPE", emptyPoint, nil} 73 | } else { 74 | inEscapeSequence = stateOUTOFSEQUENCE 75 | if escapeCanceller != nil { 76 | escapeCanceller.Do(func() { escapeCanceller = nil }) 77 | } 78 | stringChannel <- inputEvent{string(runeRead), emptyPoint, nil} 79 | } 80 | } else if inEscapeSequence == stateDIRECTIVE { 81 | switch runeRead { 82 | case 'A': 83 | stringChannel <- inputEvent{"UP", emptyPoint, nil} 84 | case 'B': 85 | stringChannel <- inputEvent{"DOWN", emptyPoint, nil} 86 | case 'C': 87 | stringChannel <- inputEvent{"RIGHT", emptyPoint, nil} 88 | case 'D': 89 | stringChannel <- inputEvent{"LEFT", emptyPoint, nil} 90 | case 'M': 91 | code, err := reader.ReadByte() 92 | if err != nil { 93 | cancel() 94 | return 95 | } 96 | 97 | nx, _ := reader.ReadByte() 98 | ny, _ := reader.ReadByte() 99 | 100 | pt := Point{X: uint32(nx) - 32, Y: uint32(ny) - 32} 101 | 102 | event := "" 103 | 104 | switch code { 105 | case 32: 106 | event = "MOUSEDOWN" 107 | case 33: 108 | event = "MIDDLEDOWN" 109 | case 35: 110 | event = "MOUSEUP" 111 | case 67: 112 | event = "MOUSEMOVE" 113 | case 96: 114 | event = "SCROLLUP" 115 | case 97: 116 | event = "SCROLLDOWN" 117 | } 118 | 119 | if len(event) > 0 { 120 | stringChannel <- inputEvent{event, pt, nil} 121 | } 122 | default: 123 | stringChannel <- inputEvent{strconv.QuoteRune(runeRead), emptyPoint, nil} 124 | } 125 | inEscapeSequence = stateOUTOFSEQUENCE 126 | } else { 127 | if newString, ok := codeMap[runeRead]; ok { 128 | stringChannel <- inputEvent{newString, emptyPoint, nil} 129 | } else { 130 | stringChannel <- inputEvent{string(runeRead), emptyPoint, nil} 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /internal/terrain.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | ) 8 | 9 | // DefaultBiomeType yes 10 | const DefaultBiomeType = "open-grass" 11 | 12 | // BiomeData contains information about biome types 13 | type BiomeData struct { 14 | ID string 15 | Name string `json:""` 16 | Algorithm string `json:""` // Need strategies to make land 17 | AlgorithmParameters map[string]string `json:""` // Helpers for terrain generator algorithm 18 | Transitions []string `json:""` // Other biome types this can transition into when generating 19 | GetRandomTransition func() string // What to transition to 20 | } 21 | 22 | // DefaultCellType is the seed land type when spawning a character. 23 | const DefaultCellType string = "clearing" 24 | 25 | // CellTerrain stores rules about different cell's terrain types. 26 | // For 256 color colors check https://jonasjacek.github.io/colors/ 27 | type CellTerrain struct { 28 | ID string `json:""` 29 | Permeable bool `json:""` // Things like paths, rivers, etc. should be permeable so biomes don't suddenly stop geneating through them. 30 | Blocking bool `json:""` // Some terrain types are impassable; e.g. walls 31 | Name string `json:",omitempty"` // Formatstring to modify place name 32 | Algorithm string `json:""` // Should have algos for e.g. town grid building etc. 33 | AlgorithmParameters map[string]string `json:""` // Helpers for terrain generator algorithm 34 | CreatureSpawns []CreatureSpawn `json:""` // List of monster types and probabilities of them appearing in each terrain type 35 | ItemDrops []ItemDrop `json:""` // List of items and probabilities of them appearing in each terrain type 36 | FGcolor byte `json:""` // SSH-display specific: the 256 color xterm color for FG 37 | BGcolor byte `json:""` // SSH-display specific: the 256 color xterm color for BG 38 | Bold bool `json:""` // SSH-display specific: bold the cell FG? 39 | Animated bool `json:""` // SSH-display specific: Fake an animation effect? 40 | Representations []rune `json:""` // SSH-display specific: unicode chars to use to represent this cell on-screen 41 | } 42 | 43 | // CellTypes is the list of cell types 44 | var CellTypes map[string]CellTerrain 45 | 46 | // BiomeTypes is the list of cell types 47 | var BiomeTypes map[string]BiomeData 48 | 49 | // NORTHBIT North for bitmasks 50 | // EASTBIT East for bitmasks 51 | // SOUTHBIT South for bitmasks 52 | // WESTBIT West for bitmasks 53 | const ( 54 | NORTHBIT = 1 55 | EASTBIT = 2 56 | SOUTHBIT = 4 57 | WESTBIT = 8 58 | ) 59 | 60 | // CellInfo holds more information on the cell: exits, items available, etc. 61 | type CellInfo struct { 62 | TerrainID string `json:""` 63 | TerrainData CellTerrain `json:"-"` 64 | BiomeID string `json:""` 65 | BiomeData BiomeData `json:"-"` 66 | ExitBlocks byte `json:""` 67 | RegionNameID uint64 `json:""` 68 | RegionName string `json:"-"` 69 | } 70 | 71 | func loadTerrainTypes(terrainInfoFile string) { 72 | data, err := ioutil.ReadFile(terrainInfoFile) 73 | 74 | var terrainFileData struct { 75 | CellTypes map[string]CellTerrain `json:"cells"` 76 | BiomeTypes map[string]BiomeData `json:"biomes"` 77 | } 78 | 79 | if err == nil { 80 | err = json.Unmarshal(data, &terrainFileData) 81 | 82 | BiomeTypes = make(map[string]BiomeData) 83 | for k, val := range terrainFileData.BiomeTypes { 84 | val.ID = k 85 | val.GetRandomTransition, val.Transitions = MakeTransitionFunction(val.ID, val.Transitions) 86 | BiomeTypes[k] = val 87 | } 88 | 89 | CellTypes = make(map[string]CellTerrain) 90 | for k, val := range terrainFileData.CellTypes { 91 | val.ID = k 92 | CellTypes[k] = val 93 | } 94 | } 95 | 96 | if err != nil { 97 | log.Printf("Error parsing %s: %v", terrainInfoFile, err) 98 | } 99 | } 100 | 101 | func init() { 102 | CellTypes = make(map[string]CellTerrain) 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MUD Server 2 | A multiplayer MUD sever for a game jam on itch.io: [Enter the (Multi-User) Dungeon](https://itch.io/jam/enterthemud) 3 | 4 | I have a newborn and had about 12 days to make an MVP. I mostly succeeded? 5 | 6 | ## Building 7 | 8 | You need a correctly set up Golang environment over 1.11; this proejct uses `go mod`. 9 | 10 | Run make. 11 | 12 | make 13 | 14 | Then run `bin/mud` from this folder. 15 | 16 | # Connecting to Play 17 | 18 | ## Overview 19 | 20 | This MUD is a terminal-based SSH server. You need an ssh client installed and a private key generated. This is beyond the scope of this `README`, but I'll try to set you in the right direction. 21 | 22 | ## Connecting with macOS/Linux 23 | 24 | You [can probably follow these instructions](https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/) 25 | in order to get it set up on your platform of choice. From there it's a simple matter of connecting: 26 | 27 | ssh localhost -p 2222 28 | 29 | assuming you're running the mud server locally. 30 | 31 | Your terminal needs to support 256 colors and utf-8 encoding. iTerm2 and Terminal.app on macOS both support 256 colors as does pretty much any terminal you can think of on Linux. Putty works too if you et your terminal type to `xterm-256color`. Also [here is a very detailed amount of information on terminal types](https://stackoverflow.com/questions/15375992/vim-difference-between-t-co-256-and-term-xterm-256color-in-conjunction-with-tmu/15378816#15378816) if needed. 32 | 33 | ## Connecting with Windows 34 | 35 | You'll need Putty and PuttyGen. [Follow the instructions here](https://system.cs.kuleuven.be//cs/system/security/ssh/setupkeys/putty-with-key.html) for how to make a key to connect. 36 | 37 | ## Usernames 38 | 39 | You sign in with whatever username you used to log into the server. You now *own* this username on the server and nobody else can use it. No passwords! How nice! Hooray for encryption. You can also claim other usernames by logging in as other users; e.g. `ssh "Another User"@localhost -p 2222`. 40 | 41 | # Scaling 42 | 43 | This thing appears to just sip ram (idling at approx 35 megs with three users conencted on my MacBook Pro). Go as a language was designed to handle networked servers extremely well so I don't see why a local server on modest hardware wouldn't be able to host a good hundred or so users online at at time. 44 | 45 | # Playing 46 | 47 | ## Game mechanics 48 | 49 | ### Strengths 50 | 51 | Your primary and secondary strength determine how you are able to attack and defend. 52 | 53 | For instance, a sword attack is a meelee action. Throwing a grappling hook is a range action. Casting heal is a magic action. Combination actions are things like casting fireball (magic/range), shooting an arrow (range/melee), or using an enchanted staff (melee/magic). 54 | 55 | Note you can pick the same primary and secondary, which will greatly boost that individual strength. 56 | 57 | **Melee**: Strength is in physical action. Hand-to-hand combat, moving large objects. 58 | 59 | **Range**: Strength is in manipulating items from a distance. Accuracy in hitting things from far away, observing far away surroundings. 60 | 61 | **Magic**: Strength is in non-physical magical craft. Casting defensive and healing spells. 62 | 63 | The layout of the Melee/Range/Magic system is similar to Rock/Paper/Scissors: a Melee attack beats a Magic defense, a Magic offense trumps a Ranged defense, a Ranged offense beats a Melee defense. 64 | 65 | ### Skills 66 | 67 | This is not fully fleshed out. Ignore for now, subject to major changes. 68 | 69 | ## Battle 70 | 71 | You are equipped with *charge points* based on your level. Every second one charge point renews; and when your charge points are full every 5 seconds your HP will begin to restore itself. Charge points reset to zero every time you act. Moving, attacking, and changing equipment are all considering acting. 72 | 73 | You're equipped with attacks based on the strengths you chose when starting your character and may be given additional items/buffs based on class. 74 | 75 | # Keyboard commands 76 | 77 | `up`, `down`, `left`, `right`: move your character in that direction. 78 | 79 | `ctrl-c`: log off. 80 | 81 | `tab`: toggle log/inventory view. 82 | 83 | `esc`: toggle input mode. 84 | 85 | `/`: activate command input mode (any input message that starts with `/` is treated as a command). 86 | 87 | `t`: activate chat input mode (any input string that starts with `!` is treated as a chat) 88 | 89 | > **Note:** No commands have been implemented yet. -------------------------------------------------------------------------------- /internal/sshserver.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "time" 10 | 11 | gossh "golang.org/x/crypto/ssh" 12 | 13 | "github.com/gliderlabs/ssh" 14 | ) 15 | 16 | const mudPubkey = "MUD-pubkey" 17 | 18 | func handleConnection(builder WorldBuilder, session ssh.Session) { 19 | user := builder.GetUser(session.User()) 20 | screen := NewSSHScreen(session, builder, user) 21 | pubKey, _ := session.Context().Value(mudPubkey).(string) 22 | userSSH, ok := user.(UserSSHAuthentication) 23 | 24 | builder.Chat(LogItem{Message: fmt.Sprintf("User %s has logged in", user.Username()), MessageType: MESSAGESYSTEM}) 25 | user.MarkActive() 26 | user.Act() 27 | 28 | if len(session.Command()) > 0 { 29 | session.Write([]byte("Commands are not supported.\n")) 30 | session.Close() 31 | } 32 | 33 | if ok { 34 | if userSSH.SSHKeysEmpty() { 35 | userSSH.AddSSHKey(pubKey) 36 | log.Printf("Saving SSH key for %s", user.Username()) 37 | } else if !userSSH.ValidateSSHKey(pubKey) { 38 | session.Write([]byte("This is not the SSH key verified for this user. Try another username.\n")) 39 | log.Printf("User %s doesn't use this key.", user.Username()) 40 | return 41 | } 42 | } 43 | 44 | ctx, cancel := context.WithCancel(context.Background()) 45 | 46 | logMessage := fmt.Sprintf("Logged in as %s via %s at %s", user.Username(), session.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)) 47 | log.Println(logMessage) 48 | user.Log(LogItem{Message: logMessage, MessageType: MESSAGESYSTEM}) 49 | 50 | done := session.Context().Done() 51 | tick := time.Tick(500 * time.Millisecond) 52 | tickForOnline := time.Tick(2 * time.Second) 53 | stringInput := make(chan inputEvent, 1) 54 | reader := bufio.NewReader(session) 55 | 56 | go handleKeys(reader, stringInput, cancel) 57 | 58 | if !user.IsInitialized() { 59 | setupSSHUser(ctx, cancel, done, session, user, stringInput) 60 | } 61 | 62 | for { 63 | select { 64 | case inputString := <-stringInput: 65 | if inputString.err != nil { 66 | screen.Reset() 67 | session.Close() 68 | continue 69 | } 70 | switch inputString.inputString { 71 | case "UP": 72 | builder.MoveUserNorth(user) 73 | screen.Render() 74 | case "DOWN": 75 | builder.MoveUserSouth(user) 76 | screen.Render() 77 | case "LEFT": 78 | builder.MoveUserWest(user) 79 | screen.Render() 80 | case "RIGHT": 81 | builder.MoveUserEast(user) 82 | screen.Render() 83 | case "TAB": 84 | screen.ToggleInventory() 85 | case "ESCAPE": 86 | screen.ToggleInput() 87 | case "[": 88 | if screen.InputActive() { 89 | screen.HandleInputKey(inputString.inputString) 90 | } else if screen.InventoryActive() { 91 | screen.PreviousInventoryItem() 92 | } 93 | case "]": 94 | if screen.InputActive() { 95 | screen.HandleInputKey(inputString.inputString) 96 | } else if screen.InventoryActive() { 97 | screen.NextInventoryItem() 98 | } 99 | case "/": 100 | screen.ToggleCommand() 101 | case "BACKSPACE": 102 | if screen.InputActive() { 103 | screen.HandleInputKey(inputString.inputString) 104 | } 105 | case "ENTER": 106 | if screen.InputActive() { 107 | chat := screen.GetChat() 108 | var chatItem LogItem 109 | if screen.InCommandMode() { 110 | chatItem = LogItem{ 111 | Author: user.Username(), 112 | Message: chat, 113 | MessageType: MESSAGEACTION} 114 | if len(chat) > 0 { 115 | user.Log(chatItem) 116 | } 117 | } else { 118 | chatItem = LogItem{ 119 | Author: user.Username(), 120 | Message: chat, 121 | MessageType: MESSAGECHAT} 122 | if len(chat) > 0 { 123 | builder.Chat(chatItem) 124 | } 125 | } 126 | 127 | screen.Render() 128 | } 129 | default: 130 | screen.HandleInputKey(inputString.inputString) 131 | } 132 | case <-ctx.Done(): 133 | cancel() 134 | case <-tickForOnline: 135 | user.MarkActive() 136 | case <-tick: 137 | user.Reload() 138 | screen.Render() 139 | continue 140 | case <-done: 141 | log.Printf("Disconnected %v@%v", user.Username(), session.RemoteAddr()) 142 | user.Log(LogItem{Message: fmt.Sprintf("Signed off at %v", time.Now().UTC().Format(time.RFC3339)), 143 | MessageType: MESSAGESYSTEM}) 144 | screen.Reset() 145 | session.Close() 146 | return 147 | } 148 | } 149 | } 150 | 151 | // ServeSSH runs the main SSH server loop. 152 | func ServeSSH(listen string) { 153 | rand.Seed(time.Now().Unix()) 154 | 155 | world := LoadWorldFromDB("./world.db") 156 | defer world.Close() 157 | builder := NewWorldBuilder(world) 158 | 159 | privateKey := makeKeyFiles() 160 | 161 | publicKeyOption := ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { 162 | marshal := gossh.MarshalAuthorizedKey(key) 163 | ctx.SetValue(mudPubkey, string(marshal)) 164 | return true 165 | }) 166 | 167 | log.Printf("Starting SSH server on %v", listen) 168 | log.Fatal(ssh.ListenAndServe(listen, func(s ssh.Session) { 169 | handleConnection(builder, s) 170 | }, publicKeyOption, ssh.HostKeyFile(privateKey))) 171 | } 172 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ahmetb/go-cursor v0.0.0-20131010032410-8136607ea412 h1:mjEdk5IWaOUyDfmIScVahVtW56YQ1gBv8RMyHl69Z30= 2 | github.com/ahmetb/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:6/fH+MoHXlGOc3iy8TSNB4eM1oaBDMs1oxPVN40M3h0= 3 | github.com/andlabs/ui v0.0.0-20200610043537-70a69d6ae31e h1:wSQCJiig/QkoUnpvelSPbLiZNWvh2yMqQTQvIQqSUkU= 4 | github.com/andlabs/ui v0.0.0-20200610043537-70a69d6ae31e/go.mod h1:5G2EjwzgZUPnnReoKvPWVneT8APYbyKkihDVAHUi0II= 5 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 6 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 7 | github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw= 8 | github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= 9 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 10 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 12 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 14 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 15 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 16 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 17 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 18 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 19 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 20 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 21 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 22 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 23 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 24 | github.com/ojrac/opensimplex-go v1.0.2 h1:l4vs0D+JCakcu5OV0kJ99oEaWJfggSc9jiLpxaWvSzs= 25 | github.com/ojrac/opensimplex-go v1.0.2/go.mod h1:NwbXFFbXcdGgIFdiA7/REME+7n/lOf1TuEbLiZYOWnM= 26 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 27 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 28 | go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= 29 | go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 30 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 31 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 32 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= 33 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 34 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 35 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 36 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 37 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= 45 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 47 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 50 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 51 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 52 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 53 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 54 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 56 | -------------------------------------------------------------------------------- /internal/battle.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // StatInfo handles user/NPC stats 12 | type StatInfo interface { 13 | HP() uint64 14 | SetHP(uint64) 15 | MP() uint64 16 | SetMP(uint64) 17 | AP() uint64 18 | SetAP(uint64) 19 | RP() uint64 20 | SetRP(uint64) 21 | MaxHP() uint64 22 | SetMaxHP(uint64) 23 | MaxMP() uint64 24 | SetMaxMP(uint64) 25 | MaxAP() uint64 26 | SetMaxAP(uint64) 27 | MaxRP() uint64 28 | SetMaxRP(uint64) 29 | XP() uint64 30 | AddXP(uint64) 31 | XPToNextLevel() uint64 32 | } 33 | 34 | // GetStatPoints is for StatPointable 35 | func GetStatPoints(statinfo StatInfo) StatPoints { 36 | return StatPoints{ 37 | AP: statinfo.MaxAP(), 38 | RP: statinfo.MaxRP(), 39 | MP: statinfo.MaxMP()} 40 | } 41 | 42 | // StatPointable lets an item return StatInfos for battle calculations 43 | type StatPointable interface { 44 | StatPoints() StatPoints 45 | } 46 | 47 | // FullStatPointable is more or less the same thing 48 | type FullStatPointable interface { 49 | FullStatPoints() FullStatPoints 50 | } 51 | 52 | // StatPoints is a passable struct for managing attack/defense calculations 53 | type StatPoints struct { 54 | AP uint64 55 | RP uint64 56 | MP uint64 57 | } 58 | 59 | // FullStatPoints is a passable struct for attack/defense that does EVERYTHING. 60 | type FullStatPoints struct { 61 | StatPoints 62 | Trample uint64 63 | HP uint64 64 | } 65 | 66 | // ApplyDefense takes two StatPoints to find how much attack points apply 67 | func (input *StatPoints) ApplyDefense(apply *StatPoints) *StatPoints { 68 | if input == nil || apply == nil { 69 | return nil 70 | } 71 | 72 | resolved := StatPoints{ 73 | AP: input.AP, 74 | RP: input.RP, 75 | MP: input.MP} 76 | 77 | if input.AP <= apply.RP { 78 | resolved.AP = 0 79 | } else { 80 | resolved.AP -= apply.RP 81 | } 82 | 83 | if input.RP <= apply.MP { 84 | resolved.RP = 0 85 | } else { 86 | resolved.RP -= apply.MP 87 | } 88 | 89 | if input.MP <= apply.AP { 90 | resolved.MP = 0 91 | } else { 92 | resolved.MP -= apply.AP 93 | } 94 | 95 | return &resolved 96 | } 97 | 98 | // Damage is how much untyped damage is dealt via this StatPoints 99 | func (input *StatPoints) Damage() uint64 { 100 | return input.AP + input.MP + input.RP 101 | } 102 | 103 | // Attack is a type of attack a creature can inflict 104 | type Attack struct { 105 | ID string `json:",omitempty"` 106 | Name string `json:""` 107 | Accuracy byte `json:""` 108 | MP uint64 `json:""` 109 | AP uint64 `json:""` 110 | RP uint64 `json:""` 111 | Trample uint64 `json:""` 112 | Bonuses string `json:""` 113 | UsesItems []string `json:",omitempty"` 114 | OutputsItems []string `json:",omitempty"` 115 | Effects []string `json:""` 116 | Charge int64 `json:""` // In Seconds 117 | } 118 | 119 | func (atk *Attack) String() string { 120 | return fmt.Sprintf("%v: AP:%v RP:%v MP:%v", atk.Name, atk.AP, atk.RP, atk.MP) 121 | } 122 | 123 | // StatPoints gets a stripped down statinfo object for battle calculation arithmetic 124 | func (atk *Attack) StatPoints() StatPoints { 125 | return StatPoints{ 126 | AP: atk.AP, 127 | RP: atk.RP, 128 | MP: atk.MP} 129 | } 130 | 131 | // FullStatPoints gets a fullstatinfo object for battle calculation arithmetic 132 | func (atk *Attack) FullStatPoints() FullStatPoints { 133 | return FullStatPoints{ 134 | StatPoints: StatPoints{ 135 | AP: atk.AP, 136 | RP: atk.RP, 137 | MP: atk.MP}, 138 | Trample: atk.Trample} 139 | } 140 | 141 | // Copies stat points back in after applying bonuses 142 | func (atk *Attack) applyStatPoints(sp FullStatPoints) Attack { 143 | newAtk := *atk 144 | 145 | newAtk.AP = sp.AP 146 | newAtk.RP = sp.AP 147 | newAtk.MP = sp.MP 148 | newAtk.Trample = sp.Trample 149 | 150 | return newAtk 151 | } 152 | 153 | // ApplyBonuses Apply any bonuses to attack stats based on bonus string 154 | func (atk *Attack) ApplyBonuses(sp FullStatPointable) Attack { 155 | statSP := sp.FullStatPoints() 156 | atkSP := atk.FullStatPoints() 157 | 158 | newStats := ApplyBonuses(&atkSP, &statSP, atk.Bonuses) 159 | 160 | atkCopy := *atk 161 | return atkCopy.applyStatPoints(newStats) 162 | } 163 | 164 | func getModifierFunctions(field string, statPoints *FullStatPoints, applyTo *FullStatPoints) (func() uint64, func(uint64)) { 165 | if applyTo == nil { 166 | applyTo = statPoints 167 | } 168 | 169 | switch field { 170 | case "HP": 171 | return func() uint64 { return statPoints.HP }, func(value uint64) { applyTo.HP += value } 172 | case "TP": 173 | return func() uint64 { return statPoints.Trample }, func(value uint64) { applyTo.Trample += value } 174 | case "AP": 175 | return func() uint64 { return statPoints.StatPoints.AP }, func(value uint64) { applyTo.StatPoints.AP += value } 176 | case "RP": 177 | return func() uint64 { return statPoints.StatPoints.RP }, func(value uint64) { applyTo.StatPoints.RP += value } 178 | case "MP": 179 | return func() uint64 { return statPoints.StatPoints.MP }, func(value uint64) { applyTo.StatPoints.MP += value } 180 | } 181 | 182 | return nil, nil 183 | } 184 | 185 | // ApplyBonuses Apply any bonuses to attack stats based on bonus string 186 | func ApplyBonuses(statPoints *FullStatPoints, applyTo *FullStatPoints, bonuses string) FullStatPoints { 187 | modifiedStats := *statPoints 188 | 189 | fieldRE := regexp.MustCompile("^([HARMT]P)") 190 | modstringRE := regexp.MustCompile("([+-])([0-9]+)([%]?)([HARMT]P)?") 191 | 192 | for _, modifier := range strings.Split(bonuses, ";") { 193 | modField := fieldRE.FindString(modifier) 194 | modifiers := modstringRE.FindAllStringSubmatch(modifier, -1) 195 | 196 | for _, modifier := range modifiers { 197 | operand := modifier[1] 198 | numberAsString := modifier[2] 199 | optionalPercentage := modifier[3] 200 | fromField := modifier[4] 201 | 202 | number, _ := strconv.Atoi(numberAsString) 203 | 204 | multiplier := 1 205 | if operand == "-" { 206 | multiplier = -1 207 | } 208 | 209 | if optionalPercentage != "" { 210 | if fromField == "" { 211 | fromField = modField 212 | } 213 | 214 | getter, _ := getModifierFunctions(fromField, &modifiedStats, applyTo) 215 | _, setter := getModifierFunctions(modField, &modifiedStats, applyTo) 216 | 217 | value := float64(getter()) * (float64(number) / 100.0) 218 | setter(uint64(float64(multiplier) * value)) 219 | } else { 220 | if fromField == "" && numberAsString != "" { 221 | _, setter := getModifierFunctions(modField, &modifiedStats, applyTo) 222 | setter(uint64(int(number) * multiplier)) 223 | } else if fromField != "" && numberAsString == "" { 224 | _, setter := getModifierFunctions(modField, &modifiedStats, applyTo) 225 | othergetter, _ := getModifierFunctions(fromField, &modifiedStats, applyTo) 226 | setter(uint64(int(othergetter()) * multiplier)) 227 | } else { 228 | log.Printf("Illegal modifier: %v", modifier) 229 | } 230 | } 231 | } 232 | } 233 | 234 | return modifiedStats 235 | } 236 | -------------------------------------------------------------------------------- /internal/usersetup.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log" 9 | "math/rand" 10 | "os" 11 | "strings" 12 | "time" 13 | 14 | "github.com/mgutz/ansi" 15 | 16 | "github.com/ahmetb/go-cursor" 17 | 18 | "github.com/gliderlabs/ssh" 19 | ) 20 | 21 | type setMapThing struct { 22 | value byte 23 | name string 24 | } 25 | 26 | var primaryStrengthArray = []setMapThing{ 27 | {value: MELEEPRIMARY, name: "1: Melee"}, 28 | {value: RANGEPRIMARY, name: "2: Range"}, 29 | {value: MAGICPRIMARY, name: "3: Magic"}, 30 | } 31 | 32 | var secondaryStrengthArray = []setMapThing{ 33 | {value: MELEESECONDARY, name: "Q: Melee"}, 34 | {value: RANGESECONDARY, name: "W: Range"}, 35 | {value: MAGICSECONDARY, name: "E: Magic"}, 36 | } 37 | 38 | var primarySkillArray = []setMapThing{ 39 | {value: CUNNINGPRIMARY, name: "A: Cunning"}, 40 | {value: ORDERLYPRIMARY, name: "S: Orderly"}, 41 | {value: CREATIVEPRIMARY, name: "D: Creative"}, 42 | } 43 | 44 | var secondarySkillArray = []setMapThing{ 45 | {value: CUNNINGSECONDARY, name: "Z: Cunning"}, 46 | {value: ORDERLYSECONDARY, name: "X: Orderly"}, 47 | {value: CREATIVESECONDARY, name: "C: Creative"}, 48 | } 49 | 50 | func greet(user User) { 51 | inFile, err := os.Open("./welcome.txt") 52 | if err == nil { 53 | defer inFile.Close() 54 | scanner := bufio.NewScanner(inFile) 55 | scanner.Split(bufio.ScanLines) 56 | 57 | user.Log(LogItem{Message: "", MessageType: MESSAGEACTIVITY}) 58 | 59 | for scanner.Scan() { 60 | logString := scanner.Text() 61 | 62 | if len(logString) == 0 { 63 | user.Log(LogItem{Message: "", MessageType: MESSAGESYSTEM}) 64 | } else if logString[0] == '^' { 65 | user.Log(LogItem{Message: logString[1:], MessageType: MESSAGESYSTEM}) 66 | } else if logString[0] == '%' { 67 | items := strings.SplitN(logString, ":", 2) 68 | user.Log(LogItem{Author: items[0][1:len(items[0])], Message: strings.TrimSpace(items[1]), MessageType: MESSAGECHAT}) 69 | } else { 70 | user.Log(LogItem{Message: logString, MessageType: MESSAGEACTIVITY}) 71 | } 72 | } 73 | } 74 | } 75 | 76 | func renderChoices(selected byte, items []setMapThing) string { 77 | unselectedf := ansi.ColorFunc("white") 78 | selectedf := ansi.ColorFunc("black:white") 79 | 80 | retstring := "" 81 | for index, value := range items { 82 | if index > 0 { 83 | retstring += " ⌑ " 84 | } 85 | 86 | if value.value == selected { 87 | retstring += selectedf(value.name) 88 | } else { 89 | retstring += unselectedf(value.name) 90 | } 91 | } 92 | 93 | return retstring + ansi.ColorCode("reset") 94 | } 95 | 96 | func renderSetup(session ssh.Session, user User) { 97 | primarystrength, secondarystrength := user.Strengths() 98 | primaryskill, secondaryskill := user.Skills() 99 | 100 | strTitle, sklTitle := GetSubTitles(primarystrength, secondarystrength, primaryskill, secondaryskill) 101 | 102 | header := ansi.ColorFunc("white+b:black") 103 | title := ansi.ColorFunc("250+b:black") 104 | 105 | io.WriteString(session, cursor.ClearEntireScreen()+cursor.MoveUpperLeft(1)) 106 | io.WriteString(session, fmt.Sprintf("Please set up your character, %v.\n\n", user.Username())) 107 | 108 | io.WriteString(session, header("Strength: ")) 109 | io.WriteString(session, "\n") 110 | io.WriteString(session, " Primary: "+renderChoices(primarystrength, primaryStrengthArray)) 111 | io.WriteString(session, "\n") 112 | io.WriteString(session, " Secondary: "+renderChoices(secondarystrength, secondaryStrengthArray)) 113 | io.WriteString(session, "\n") 114 | io.WriteString(session, " Strengths: "+title(centerText(strTitle, " ", 43))) 115 | 116 | io.WriteString(session, "\n\n") 117 | 118 | io.WriteString(session, header("Skill: ")) 119 | io.WriteString(session, "\n") 120 | io.WriteString(session, " Primary: "+renderChoices(primaryskill, primarySkillArray)) 121 | io.WriteString(session, "\n") 122 | io.WriteString(session, " Secondary: "+renderChoices(secondaryskill, secondarySkillArray)) 123 | io.WriteString(session, "\n") 124 | io.WriteString(session, " Skills: "+title(centerText(sklTitle, " ", 43))) 125 | 126 | io.WriteString(session, "\n\n") 127 | io.WriteString(session, "You're a: "+title(" "+centerText(GetTitle(primarystrength, secondarystrength, primaryskill, secondaryskill), " ", 47))) 128 | io.WriteString(session, "\n") 129 | 130 | io.WriteString(session, "\n\n") 131 | io.WriteString(session, "Press enter when you are finished.") 132 | } 133 | 134 | func setupSSHUser(ctx context.Context, cancel context.CancelFunc, done <-chan struct{}, session ssh.Session, user User, stringInput chan inputEvent) { 135 | tick := time.Tick(500 * time.Millisecond) 136 | 137 | strengthPrimary := []byte{MELEEPRIMARY, RANGEPRIMARY, MAGICPRIMARY} 138 | strengthSecondary := []byte{MELEESECONDARY, RANGESECONDARY, MAGICSECONDARY} 139 | skillPrimary := []byte{CUNNINGPRIMARY, ORDERLYPRIMARY, CREATIVEPRIMARY} 140 | skillSecondary := []byte{CUNNINGSECONDARY, ORDERLYSECONDARY, CREATIVESECONDARY} 141 | 142 | user.SetClassInfo( 143 | strengthPrimary[rand.Int()%len(strengthPrimary)] | 144 | strengthSecondary[rand.Int()%len(strengthSecondary)] | 145 | skillPrimary[rand.Int()%len(skillPrimary)] | 146 | skillSecondary[rand.Int()%len(skillSecondary)]) 147 | 148 | renderSetup(session, user) 149 | 150 | for { 151 | select { 152 | case inputString := <-stringInput: 153 | primarystrength, secondarystrength := user.Strengths() 154 | primaryskill, secondaryskill := user.Skills() 155 | 156 | if inputString.err != nil { 157 | session.Close() 158 | continue 159 | } 160 | switch inputString.inputString { 161 | case "1": 162 | primarystrength = MELEEPRIMARY 163 | case "2": 164 | primarystrength = RANGEPRIMARY 165 | case "3": 166 | primarystrength = MAGICPRIMARY 167 | 168 | case "q": 169 | fallthrough 170 | case "Q": 171 | secondarystrength = MELEESECONDARY 172 | case "w": 173 | fallthrough 174 | case "W": 175 | secondarystrength = RANGESECONDARY 176 | case "e": 177 | fallthrough 178 | case "E": 179 | secondarystrength = MAGICSECONDARY 180 | 181 | case "a": 182 | fallthrough 183 | case "A": 184 | primaryskill = CUNNINGPRIMARY 185 | case "s": 186 | fallthrough 187 | case "S": 188 | primaryskill = ORDERLYPRIMARY 189 | case "d": 190 | fallthrough 191 | case "D": 192 | primaryskill = CREATIVEPRIMARY 193 | 194 | case "z": 195 | fallthrough 196 | case "Z": 197 | secondaryskill = CUNNINGSECONDARY 198 | case "x": 199 | fallthrough 200 | case "X": 201 | secondaryskill = ORDERLYSECONDARY 202 | case "c": 203 | fallthrough 204 | case "C": 205 | secondaryskill = CREATIVESECONDARY 206 | 207 | case "ESCAPE": 208 | session.Close() 209 | 210 | case "ENTER": 211 | greet(user) 212 | user.Initialize(true) 213 | return 214 | } 215 | 216 | user.SetStrengths(primarystrength, secondarystrength) 217 | user.SetSkills(primaryskill, secondaryskill) 218 | renderSetup(session, user) 219 | 220 | case <-ctx.Done(): 221 | cancel() 222 | case <-tick: 223 | user.MarkActive() 224 | case <-done: 225 | log.Printf("Disconnected setup %v", session.RemoteAddr()) 226 | user.Log(LogItem{Message: fmt.Sprintf("Canceled player setup %v", time.Now().UTC().Format(time.RFC3339)), MessageType: MESSAGESYSTEM}) 227 | session.Close() 228 | return 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /bestiary.json: -------------------------------------------------------------------------------- 1 | { 2 | "grass-snake": { 3 | "Name": "Grass Snake", 4 | "Hostile": true, 5 | "MaxHP": 4, 6 | "MaxMP": 1, 7 | "MaxAP": 1, 8 | "MaxRP": 1, 9 | "Attacks": [ 10 | { 11 | "Name": "Hiss", 12 | "Accuracy": 100, 13 | "MP": 0, 14 | "AP": 0, 15 | "RP": 0, 16 | "Charge": 1 17 | }, 18 | { 19 | "Name": "Lash", 20 | "Accuracy": 50, 21 | "MP": 0, 22 | "AP": 1, 23 | "RP": 1, 24 | "Charge": 1 25 | }, 26 | { 27 | "Name": "Strike", 28 | "Accuracy": 50, 29 | "MP": 1, 30 | "AP": 1, 31 | "RP": 0, 32 | "Trample": 1, 33 | "Charge": 2 34 | } 35 | ] 36 | }, 37 | "goat": { 38 | "Name": "Grumpy Goat", 39 | "Hostile": true, 40 | "MaxHP": 1, 41 | "MaxMP": 0, 42 | "MaxAP": 0, 43 | "MaxRP": 0, 44 | "ItemDrops": [ 45 | { 46 | "Name": "Shiny Rock", 47 | "Probability": 0.9 48 | } 49 | ], 50 | "Attacks": [] 51 | }, 52 | "scorpion": { 53 | "Name": "Scorpion", 54 | "Hostile": true, 55 | "MaxHP": 6, 56 | "MaxMP": 3, 57 | "MaxAP": 3, 58 | "MaxRP": 3, 59 | "Attacks": [ 60 | { 61 | "Name": "Skitter", 62 | "Accuracy": 100, 63 | "MP": 0, 64 | "AP": 0, 65 | "RP": 0, 66 | "Charge": 1 67 | }, 68 | { 69 | "Name": "Claw", 70 | "Accuracy": 75, 71 | "MP": 1, 72 | "AP": 2, 73 | "RP": 1, 74 | "Trample": 2, 75 | "Charge": 2 76 | }, 77 | { 78 | "Name": "Sting", 79 | "Accuracy": 95, 80 | "MP": 9, 81 | "AP": 5, 82 | "RP": 3, 83 | "Trample": 4, 84 | "Charge": 4, 85 | "Effects": [ 86 | "poison" 87 | ] 88 | } 89 | ] 90 | }, 91 | "tarantula": { 92 | "Name": "Tarantula", 93 | "Hostile": true, 94 | "MaxHP": 3, 95 | "MaxMP": 2, 96 | "MaxAP": 2, 97 | "MaxRP": 2, 98 | "Attacks": [ 99 | { 100 | "Name": "Skitter", 101 | "Accuracy": 100, 102 | "MP": 0, 103 | "AP": 0, 104 | "RP": 0, 105 | "Charge": 1 106 | }, 107 | { 108 | "Name": "Bite", 109 | "Accuracy": 75, 110 | "MP": 5, 111 | "AP": 10, 112 | "RP": 2, 113 | "Trample": 2, 114 | "Charge": 10, 115 | "Effects": [ 116 | "poison" 117 | ] 118 | }, 119 | { 120 | "Name": "Spines", 121 | "Accuracy": 25, 122 | "MP": 3, 123 | "AP": 1, 124 | "RP": 3, 125 | "Trample": 8, 126 | "Charge": 3, 127 | "Effects": [ 128 | "thorn" 129 | ] 130 | } 131 | ] 132 | }, 133 | "rat": { 134 | "Name": "Small Rat", 135 | "Hostile": true, 136 | "MaxHP": 5, 137 | "MaxMP": 3, 138 | "MaxAP": 3, 139 | "MaxRP": 3, 140 | "Attacks": [ 141 | { 142 | "Name": "Bite", 143 | "Accuracy": 100, 144 | "MP": 0, 145 | "AP": 3, 146 | "RP": 0, 147 | "Trample": 1, 148 | "Charge": 3 149 | }, 150 | { 151 | "Name": "Leap", 152 | "Accuracy": 100, 153 | "MP": 0, 154 | "AP": 0, 155 | "RP": 3, 156 | "Trample": 1, 157 | "Charge": 3 158 | }, 159 | { 160 | "Name": "Scurry", 161 | "Accuracy": 100, 162 | "MP": 3, 163 | "AP": 0, 164 | "RP": 0, 165 | "Trample": 1, 166 | "Charge": 3 167 | } 168 | ] 169 | }, 170 | "mouse": { 171 | "Name": "Mouse", 172 | "Hostile": true, 173 | "MaxHP": 1, 174 | "MaxMP": 1, 175 | "MaxAP": 1, 176 | "MaxRP": 1, 177 | "Attacks": [ 178 | { 179 | "Name": "Bite", 180 | "Accuracy": 5, 181 | "MP": 0, 182 | "AP": 0, 183 | "RP": 0, 184 | "Trample": 1, 185 | "Charge": 2 186 | }, 187 | { 188 | "Name": "Squeak", 189 | "Accuracy": 0, 190 | "MP": 0, 191 | "AP": 0, 192 | "RP": 0, 193 | "Charge": 1 194 | } 195 | ] 196 | }, 197 | "skeltal": { 198 | "Name": "Mr. Skeltal", 199 | "Hostile": true, 200 | "MaxHP": 8, 201 | "MaxMP": 4, 202 | "MaxAP": 4, 203 | "MaxRP": 4, 204 | "Attacks": [ 205 | { 206 | "Name": "Thank", 207 | "Accuracy": 0, 208 | "MP": 1, 209 | "AP": 1, 210 | "RP": 1, 211 | "Charge": 1 212 | }, 213 | { 214 | "Name": "Shake", 215 | "Accuracy": 5, 216 | "MP": 4, 217 | "AP": 1, 218 | "RP": 1, 219 | "Charge": 2 220 | }, 221 | { 222 | "Name": "Rattle", 223 | "Accuracy": 0, 224 | "MP": 1, 225 | "AP": 0, 226 | "RP": 0, 227 | "Charge": 2 228 | }, 229 | { 230 | "Name": "Roll", 231 | "Accuracy": 0, 232 | "MP": 1, 233 | "AP": 0, 234 | "RP": 3, 235 | "Charge": 2 236 | }, 237 | { 238 | "Name": "Doot doot", 239 | "Accuracy": 100, 240 | "MP": 7, 241 | "AP": 1, 242 | "RP": 1, 243 | "Trample": 5, 244 | "Charge": 8, 245 | "OutputsItems": [ 246 | "Skull" 247 | ] 248 | } 249 | ] 250 | }, 251 | "vagabond": { 252 | "Name": "Vagabond", 253 | "Hostile": true, 254 | "MaxHP": 6, 255 | "MaxMP": 4, 256 | "MaxAP": 4, 257 | "MaxRP": 4, 258 | "Attacks": [ 259 | { 260 | "Name": "Stab", 261 | "Accuracy": 75, 262 | "MP": 5, 263 | "AP": 4, 264 | "RP": 2, 265 | "Charge": 3 266 | }, 267 | { 268 | "Name": "Shank", 269 | "Accuracy": 75, 270 | "MP": 4, 271 | "AP": 3, 272 | "RP": 2, 273 | "Charge": 4 274 | }, 275 | { 276 | "Name": "Rock Throw", 277 | "Accuracy": 90, 278 | "MP": 0, 279 | "AP": 2, 280 | "RP": 6, 281 | "Charge": 5 282 | } 283 | ] 284 | }, 285 | "centipede": { 286 | "Name": "Centipede", 287 | "Hostile": true, 288 | "MaxHP": 8, 289 | "MaxMP": 4, 290 | "MaxAP": 4, 291 | "MaxRP": 4, 292 | "Attacks": [ 293 | { 294 | "Name": "Skitter", 295 | "Accuracy": 50, 296 | "MP": 1, 297 | "AP": 1, 298 | "RP": 1, 299 | "Charge": 1 300 | }, 301 | { 302 | "Name": "Bite", 303 | "Accuracy": 80, 304 | "MP": 0, 305 | "AP": 0, 306 | "RP": 0, 307 | "Charge": 6, 308 | "Effects": [ 309 | "poison" 310 | ] 311 | } 312 | ] 313 | } 314 | } -------------------------------------------------------------------------------- /internal/worldbuilder.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // MoveUser moves a user in the world; allowing the environment to intercept user movements 9 | // in case some other thing needs to happen (traps, blocking, etc) 10 | type MoveUser interface { 11 | MoveUserNorth(user User) 12 | MoveUserSouth(user User) 13 | MoveUserEast(user User) 14 | MoveUserWest(user User) 15 | } 16 | 17 | // WorldBuilder handles map generation on top of the World, which is more a data store. 18 | type WorldBuilder interface { 19 | StepInto(x1, y1, x2, y2 uint32) bool 20 | World() World 21 | GetUser(string) User 22 | Chat(LogItem) 23 | Attack(interface{}, interface{}, *Attack) 24 | 25 | MoveUser 26 | } 27 | 28 | // CellRenderInfo holds the minimum info for rendering a plot of map in a terminal 29 | type CellRenderInfo struct { 30 | FGColor byte 31 | BGColor byte 32 | Bold bool 33 | Glyph rune 34 | } 35 | 36 | // SSHInterfaceTools has miscellaneous helpers for 37 | type SSHInterfaceTools interface { 38 | GetTerrainMap(uint32, uint32, uint32, uint32) [][]CellRenderInfo 39 | } 40 | 41 | type worldBuilder struct { 42 | world World 43 | } 44 | 45 | func (builder *worldBuilder) populateAround(x, y uint32, xdelta, ydelta int) { 46 | wwidth, wheight := builder.world.GetDimensions() 47 | 48 | if x > 100 && x < wwidth-100 && y > 100 && y < wheight-100 { 49 | for i := 1; i < 25; i++ { 50 | xd := uint32(int(x) + (rand.Int()%i - (i / 2))) 51 | yd := uint32(int(y) + (rand.Int()%i - (i / 2))) 52 | 53 | if builder.world.Cell(xd, yd).CellInfo() != nil { 54 | type diff struct { 55 | x, y int 56 | } 57 | 58 | directions := []diff{diff{x: -1, y: 0}, diff{x: 1, y: 0}, diff{x: 0, y: -1}, diff{x: 0, y: 1}} 59 | movement := directions[rand.Int()%len(directions)] 60 | 61 | if builder.world.Cell(uint32(int(xd)+movement.x), uint32(int(yd)+movement.y)).CellInfo() == nil { 62 | builder.StepInto(xd, yd, uint32(int(xd)+xdelta), uint32(int(yd)+ydelta)) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | func (builder *worldBuilder) StepInto(x1, y1, x2, y2 uint32) bool { 70 | newCell := builder.world.Cell(x2, y2) 71 | returnVal := newCell == nil 72 | 73 | if newCell.IsEmpty() { 74 | currentCell := builder.world.Cell(x1, y1) 75 | 76 | returnVal = PopulateCellFromAlgorithm(currentCell, newCell, builder.world) 77 | } 78 | 79 | return returnVal 80 | } 81 | 82 | func (builder *worldBuilder) World() World { 83 | return builder.world 84 | } 85 | 86 | func (builder *worldBuilder) GetUser(username string) User { 87 | return builder.world.GetUser(username) 88 | } 89 | 90 | func (builder *worldBuilder) Chat(message LogItem) { 91 | builder.world.Chat(message) 92 | } 93 | 94 | func (builder *worldBuilder) Attack(source interface{}, target interface{}, attack *Attack) { 95 | builder.world.Attack(source, target, attack) 96 | } 97 | 98 | func (builder *worldBuilder) MoveUserNorth(user User) { 99 | location := user.Location() 100 | 101 | ci := builder.world.Cell(location.X, location.Y).CellInfo() 102 | if (ci != nil) && (ci.ExitBlocks&NORTHBIT != 0) { 103 | return 104 | } 105 | 106 | if location.Y > 0 { 107 | if builder.StepInto(location.X, location.Y, location.X, location.Y-1) { 108 | builder.world.Cell(location.X, location.Y-1).ClearCreatures() 109 | } 110 | 111 | newcell := builder.world.Cell(location.X, location.Y-1).CellInfo() 112 | 113 | ct := CellTypes[ci.TerrainID] 114 | if newcell != nil { 115 | ct = CellTypes[newcell.TerrainID] 116 | } 117 | 118 | if (newcell == nil) || (newcell.ExitBlocks&SOUTHBIT != 0 || ct.Blocking) { 119 | return 120 | } 121 | user.MoveNorth() 122 | builder.populateAround(location.X, location.Y, 0, -1) 123 | } 124 | } 125 | 126 | func (builder *worldBuilder) MoveUserSouth(user User) { 127 | location := user.Location() 128 | _, height := builder.world.GetDimensions() 129 | 130 | ci := builder.world.Cell(location.X, location.Y).CellInfo() 131 | if (ci != nil) && (ci.ExitBlocks&SOUTHBIT != 0) { 132 | return 133 | } 134 | 135 | if location.Y < height-1 { 136 | if builder.StepInto(location.X, location.Y, location.X, location.Y+1) { 137 | builder.world.Cell(location.X, location.Y+1).ClearCreatures() 138 | } 139 | 140 | newcell := builder.world.Cell(location.X, location.Y+1).CellInfo() 141 | 142 | ct := CellTypes[DefaultCellType] 143 | if newcell != nil { 144 | ct = CellTypes[newcell.TerrainID] 145 | } 146 | 147 | if (newcell == nil) || (newcell.ExitBlocks&NORTHBIT != 0 || ct.Blocking) { 148 | return 149 | } 150 | user.MoveSouth() 151 | builder.populateAround(location.X, location.Y, 0, 1) 152 | } 153 | } 154 | 155 | func (builder *worldBuilder) MoveUserEast(user User) { 156 | location := user.Location() 157 | 158 | ci := builder.world.Cell(location.X, location.Y).CellInfo() 159 | if (ci != nil) && (ci.ExitBlocks&EASTBIT != 0) { 160 | return 161 | } 162 | 163 | if location.X > 0 { 164 | if builder.StepInto(location.X, location.Y, location.X+1, location.Y) { 165 | builder.world.Cell(location.X+1, location.Y).ClearCreatures() 166 | } 167 | 168 | newcell := builder.world.Cell(location.X+1, location.Y).CellInfo() 169 | 170 | ct := CellTypes[DefaultCellType] 171 | if newcell != nil { 172 | ct = CellTypes[newcell.TerrainID] 173 | } 174 | 175 | if (newcell == nil) || (newcell.ExitBlocks&WESTBIT != 0 || ct.Blocking) { 176 | return 177 | } 178 | user.MoveEast() 179 | builder.populateAround(location.X, location.Y, 1, 0) 180 | } 181 | } 182 | 183 | func (builder *worldBuilder) MoveUserWest(user User) { 184 | location := user.Location() 185 | width, _ := builder.world.GetDimensions() 186 | 187 | ci := builder.world.Cell(location.X, location.Y).CellInfo() 188 | if (ci != nil) && (ci.ExitBlocks&WESTBIT != 0) { 189 | return 190 | } 191 | 192 | if location.X < width-1 { 193 | if builder.StepInto(location.X, location.Y, location.X-1, location.Y) { 194 | builder.world.Cell(location.X-1, location.Y).ClearCreatures() 195 | } 196 | 197 | newcell := builder.world.Cell(location.X-1, location.Y).CellInfo() 198 | 199 | ct := CellTypes[DefaultCellType] 200 | if newcell != nil { 201 | ct = CellTypes[newcell.TerrainID] 202 | } 203 | 204 | if (newcell == nil) || (newcell.ExitBlocks&EASTBIT != 0 || ct.Blocking) { 205 | return 206 | } 207 | user.MoveWest() 208 | builder.populateAround(location.X, location.Y, -1, 0) 209 | } 210 | } 211 | 212 | func (builder *worldBuilder) GetTerrainMap(cx, cy, width, height uint32) [][]CellRenderInfo { 213 | terrainMap := make([][]CellRenderInfo, height) 214 | for i := range terrainMap { 215 | terrainMap[i] = make([]CellRenderInfo, width) 216 | } 217 | 218 | startx := cx - (width / uint32(2)) 219 | starty := cy - (height / uint32(2)) 220 | 221 | worldWidth, worldHeight := builder.world.GetDimensions() 222 | 223 | for xd := int64(0); xd < int64(width); xd++ { 224 | for yd := int64(0); yd < int64(height); yd++ { 225 | if (int64(startx)+xd) >= 0 && (int64(startx)+xd) < int64(worldWidth) && (int64(starty)+yd) >= 0 && (int64(starty)+yd) < int64(worldHeight) { 226 | xcoord, ycoord := uint32(int64(startx)+xd), uint32(int64(starty)+yd) 227 | cellInfo := builder.world.Cell(xcoord, ycoord).CellInfo() 228 | 229 | if cellInfo != nil { 230 | terrainInfo := cellInfo.TerrainData 231 | 232 | renderGlyph := rune('·') 233 | if cellInfo != nil && len(terrainInfo.Representations) > 0 { 234 | index := int64(xcoord ^ ycoord) 235 | if terrainInfo.Animated { 236 | index += time.Now().Unix() 237 | } 238 | renderGlyph = terrainInfo.Representations[uint32(index)%uint32(len(terrainInfo.Representations))] 239 | } else { 240 | terrainInfo.FGcolor = 232 241 | terrainInfo.BGcolor = 233 242 | } 243 | 244 | if cellInfo.TerrainData.Blocking == false { 245 | hasItems := false 246 | if builder.world.Cell(uint32(int64(startx)+xd), uint32(int64(starty)+yd)).HasInventoryItems() { 247 | hasItems = true 248 | terrainInfo.FGcolor = 178 249 | renderGlyph = rune('≡') 250 | terrainInfo.Bold = true 251 | } 252 | 253 | if builder.world.Cell(uint32(int64(startx)+xd), uint32(int64(starty)+yd)).HasCreatures() { 254 | if hasItems { 255 | terrainInfo.FGcolor = 175 256 | renderGlyph = rune('≜') 257 | } else { 258 | terrainInfo.FGcolor = 172 259 | renderGlyph = rune('∆') 260 | terrainInfo.Bold = true 261 | } 262 | } 263 | } 264 | 265 | terrainMap[yd][xd] = CellRenderInfo{ 266 | FGColor: terrainInfo.FGcolor, 267 | BGColor: terrainInfo.BGcolor, 268 | Bold: terrainInfo.Bold, 269 | Glyph: renderGlyph} 270 | } 271 | } 272 | } 273 | } 274 | 275 | for _, player := range builder.world.OnlineUsers() { 276 | location := player.Location() 277 | if location.X >= startx && location.X < startx+width && location.Y >= starty && location.Y < starty+height { 278 | ix := location.X - startx 279 | iy := location.Y - starty 280 | 281 | terrainMap[iy][ix].FGColor = 160 282 | switch terrainMap[iy][ix].Glyph { 283 | case rune('⁂'): 284 | continue 285 | case rune('⁑'): 286 | terrainMap[iy][ix].Glyph = rune('⁂') 287 | case rune('*'): 288 | terrainMap[iy][ix].Glyph = rune('⁑') 289 | default: 290 | terrainMap[iy][ix].Glyph = rune('*') 291 | } 292 | } 293 | } 294 | 295 | return terrainMap 296 | } 297 | 298 | // NewWorldBuilder creates a new WorldBuilder to surround the World 299 | func NewWorldBuilder(world World) WorldBuilder { 300 | return &worldBuilder{world: world} 301 | } 302 | -------------------------------------------------------------------------------- /internal/classsystem.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | var strengthClassNameMap map[byte]string 4 | var skillClassNameMap map[byte]string 5 | var fullTitleMap map[byte]string 6 | 7 | // Strengths 8 | const ( 9 | MELEESECONDARY = byte(1) 10 | RANGESECONDARY = byte(2) 11 | MAGICSECONDARY = byte(3) 12 | MELEEPRIMARY = byte(4) 13 | RANGEPRIMARY = byte(8) 14 | MAGICPRIMARY = byte(12) 15 | ) 16 | 17 | // Skills 18 | const ( 19 | CUNNINGSECONDARY = byte(16) 20 | ORDERLYSECONDARY = byte(32) 21 | CREATIVESECONDARY = byte(48) 22 | CUNNINGPRIMARY = byte(64) 23 | ORDERLYPRIMARY = byte(128) 24 | CREATIVEPRIMARY = byte(192) 25 | ) 26 | 27 | // Masks for strengths/skills 28 | const ( 29 | SECONDARYSTRENGTHMASK = byte(3) 30 | PRIMARYSTRENGTHMASK = byte(12) 31 | SECONDARYSKILLMASK = byte(48) 32 | PRIMARYSKILLMASK = byte(192) 33 | ) 34 | 35 | // ClassInfo handles user/NPC class orientation 36 | type ClassInfo interface { 37 | ClassInfo() byte 38 | SetClassInfo(byte) 39 | 40 | Strengths() (byte, byte) 41 | SetStrengths(byte, byte) 42 | Skills() (byte, byte) 43 | SetSkills(byte, byte) 44 | } 45 | 46 | // GetSubTitles takes strengths and gives a class title 47 | func GetSubTitles(strengthPrimary, strengthSecondary, skillPrimary, skillSecondary byte) (string, string) { 48 | strS, sklS := "Hippopotamus", "Spaghetti" 49 | 50 | strK, sK := strengthClassNameMap[strengthPrimary|strengthSecondary] 51 | 52 | if sK { 53 | strS = strK 54 | } 55 | 56 | sklK, lK := skillClassNameMap[skillPrimary|skillSecondary] 57 | 58 | if lK { 59 | sklS = sklK 60 | } 61 | 62 | return strS, sklS 63 | } 64 | 65 | // GetTitle takes strengths and gives a class title 66 | func GetTitle(strengthPrimary, strengthSecondary, skillPrimary, skillSecondary byte) string { 67 | title := "Unworthy" 68 | 69 | newTitle, ok := fullTitleMap[strengthPrimary|strengthSecondary|skillPrimary|skillSecondary] 70 | 71 | if ok { 72 | title = newTitle 73 | } 74 | 75 | return title 76 | } 77 | 78 | func init() { 79 | strengthClassNameMap = map[byte]string{ 80 | MELEEPRIMARY | MELEESECONDARY: "Warrior", 81 | MELEEPRIMARY | MAGICSECONDARY: "Paladin", 82 | MAGICPRIMARY | MELEESECONDARY: "Cleric", 83 | MAGICPRIMARY | MAGICSECONDARY: "Mage", 84 | MAGICPRIMARY | RANGESECONDARY: "Warlock", 85 | RANGEPRIMARY | MAGICSECONDARY: "Caster", 86 | RANGEPRIMARY | RANGESECONDARY: "Sniper", 87 | RANGEPRIMARY | MELEESECONDARY: "Archer", 88 | MELEEPRIMARY | RANGESECONDARY: "Ranger"} 89 | 90 | skillClassNameMap = map[byte]string{ 91 | CREATIVEPRIMARY | CREATIVESECONDARY: "Artist", 92 | CREATIVEPRIMARY | CUNNINGSECONDARY: "Performer", 93 | CUNNINGPRIMARY | CREATIVESECONDARY: "Hawker", 94 | CUNNINGPRIMARY | CUNNINGSECONDARY: "Orator", 95 | CUNNINGPRIMARY | ORDERLYSECONDARY: "Hunter", 96 | ORDERLYPRIMARY | CUNNINGSECONDARY: "Merchant", 97 | ORDERLYPRIMARY | ORDERLYSECONDARY: "Adviser", 98 | ORDERLYPRIMARY | CREATIVESECONDARY: "Scholar", 99 | CREATIVEPRIMARY | ORDERLYSECONDARY: "Engineer"} 100 | 101 | fullTitleMap = map[byte]string{ 102 | MELEEPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Fighter", 103 | MELEEPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Swordslinger", 104 | MELEEPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Swordsknecht", 105 | MELEEPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Strategist", 106 | MELEEPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Tracker", 107 | MELEEPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Pilgrim", 108 | MELEEPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Monk", 109 | MELEEPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Knight", 110 | MELEEPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Sapper", 111 | MELEEPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Mist Paladin", 112 | MELEEPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Wind Paladin", 113 | MELEEPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Wolf Paladin", 114 | MELEEPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Grey Paladin", 115 | MELEEPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Dark Paladin", 116 | MELEEPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Trail Paladin", 117 | MELEEPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Green Paladin", 118 | MELEEPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Owl Paladin", 119 | MELEEPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "White Paladin", 120 | MAGICPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Cleric of Eight-Arms", 121 | MAGICPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Fox-Cleric", 122 | MAGICPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Wolf-Cleric", 123 | MAGICPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Coyote-in-Cloth", 124 | MAGICPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Ravenscloth", 125 | MAGICPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Cleric of the Trail", 126 | MAGICPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Spinner-Cleric", 127 | MAGICPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Cleric of Dancing Flames", 128 | MAGICPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Eagle-Cleric", 129 | MAGICPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Weaver Mage", 130 | MAGICPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Storyteller Mage", 131 | MAGICPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Mindthief Mage", 132 | MAGICPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Mage of Flows", 133 | MAGICPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Mageseeker", 134 | MAGICPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Mage of Fortune", 135 | MAGICPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Seer Mage", 136 | MAGICPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Master Mage", 137 | MAGICPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Mage of the Flame", 138 | MAGICPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Mist Warlock", 139 | MAGICPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Warlock-Enthraller", 140 | MAGICPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Flickering Warlock", 141 | MAGICPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Shifting Warlock", 142 | MAGICPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Warlock of Waters", 143 | MAGICPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Warlock of Fortune", 144 | MAGICPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Warlock of the Stone", 145 | MAGICPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Warlock Dilettante", 146 | MAGICPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Warlock of Flame", 147 | RANGEPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Acolyte of Clouds", 148 | RANGEPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Acolyte of the Winds", 149 | RANGEPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Acolyte of the Liminal", 150 | RANGEPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Disciple of the Shifing Sands", 151 | RANGEPRIMARY | MAGICSECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Disciple of the Waters", 152 | RANGEPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Disciple of the Trail", 153 | RANGEPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Acolyte of Stone", 154 | RANGEPRIMARY | MAGICSECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Friend of the Sprite", 155 | RANGEPRIMARY | MAGICSECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Acolyte of Flame", 156 | RANGEPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Cloaked-in-Mist", 157 | RANGEPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Fox-in-Wood", 158 | RANGEPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Wolf-in-Shadows", 159 | RANGEPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Trapsetter", 160 | RANGEPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Raven-in-Air", 161 | RANGEPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Spy", 162 | RANGEPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Stone-in-Mountain", 163 | RANGEPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Owl-eyed", 164 | RANGEPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Demolitionist", 165 | RANGEPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Cloud Archer", 166 | RANGEPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Bow Adventurer", 167 | RANGEPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Kastspeerknecht", 168 | RANGEPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Assassin", 169 | RANGEPRIMARY | MELEESECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Trapper", 170 | RANGEPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Archer of Fortune", 171 | RANGEPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Skill Shot", 172 | RANGEPRIMARY | MELEESECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Bear Ranger", 173 | RANGEPRIMARY | MELEESECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Eagle-eyed Ranger", 174 | MELEEPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | CREATIVESECONDARY: "Vagabond", 175 | MELEEPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | CUNNINGSECONDARY: "Fox-Rogue", 176 | MELEEPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | CREATIVESECONDARY: "Spearknecht", 177 | MELEEPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | CUNNINGSECONDARY: "Rogue, Acolyte of the Flow", 178 | MELEEPRIMARY | RANGESECONDARY | CUNNINGPRIMARY | ORDERLYSECONDARY: "Rogue, Warden of the Hunt", 179 | MELEEPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | CUNNINGSECONDARY: "Rogue of Fortune", 180 | MELEEPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | ORDERLYSECONDARY: "Scout", 181 | MELEEPRIMARY | RANGESECONDARY | ORDERLYPRIMARY | CREATIVESECONDARY: "Ranger, Teller of Tales", 182 | MELEEPRIMARY | RANGESECONDARY | CREATIVEPRIMARY | ORDERLYSECONDARY: "Ranger, Warden of Stone"} 183 | } 184 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "math" 8 | "math/rand" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/vmihailenco/msgpack" 14 | ) 15 | 16 | // MessageType is a log message line type 17 | type MessageType int 18 | 19 | // Message types for log items 20 | const ( 21 | MESSAGESYSTEM MessageType = iota 22 | MESSAGECHAT 23 | MESSAGEACTION 24 | MESSAGEACTIVITY 25 | ) 26 | 27 | // LogItem is individual chat log line 28 | type LogItem struct { 29 | Message string `json:""` 30 | Author string `json:""` 31 | Timestamp time.Time `json:""` 32 | MessageType MessageType `json:""` 33 | Location *Point `json:",omit"` 34 | } 35 | 36 | // Point represents an (X,Y) pair in the world 37 | type Point struct { 38 | X uint32 39 | Y uint32 40 | } 41 | 42 | // Neighbor returns a box in that direction 43 | func (p *Point) Neighbor(d Direction) Point { 44 | return p.Add(VectorForDirection[d]) 45 | } 46 | 47 | // Add applies a vector to a point 48 | func (p *Point) Add(v Vector) Point { 49 | return Point{ 50 | X: uint32(int(p.X) + v.X), 51 | Y: uint32(int(p.Y) + v.Y)} 52 | } 53 | 54 | // Vector Gets the vector between two points such that v = p.Vector(q); p.Add(v) == q 55 | func (p *Point) Vector(v Point) Vector { 56 | return Vector{ 57 | X: int(v.X) - int(p.X), 58 | Y: int(v.Y) - int(p.Y)} 59 | } 60 | 61 | // Bresenham uses Bresenham's algorithm to visit every involved frame 62 | func (p *Point) Bresenham(v Point, visitor func(Point) error) { 63 | x0, y0 := p.X, p.Y 64 | x1, y1 := v.X, v.Y 65 | 66 | if x1 < x0 { 67 | x0, y0, x1, y1 = x1, y1, x0, y0 68 | } 69 | 70 | if x0 == x1 { // Vertical line 71 | if y0 > y1 { 72 | y0, y1 = y1, y0 73 | } 74 | 75 | for y := y0; y <= y1; y++ { 76 | if visitor(Point{X: x0, Y: y}) != nil { 77 | return 78 | } 79 | } 80 | 81 | return 82 | } else if y0 == y1 { // Horizontal line 83 | if x0 > x1 { 84 | x0, x1 = x1, x0 85 | } 86 | 87 | for x := x0; x <= x1; x++ { 88 | if visitor(Point{X: x, Y: y0}) != nil { 89 | return 90 | } 91 | } 92 | 93 | return 94 | } 95 | 96 | deltax := x1 - x0 97 | deltay := y1 - y0 98 | deltaerr := math.Abs(float64(deltay) / float64(deltax)) 99 | err := float64(0.0) 100 | y := y0 101 | 102 | signDeltaY := int(1) 103 | if math.Signbit(float64(deltay)) { 104 | signDeltaY = -1 105 | } 106 | 107 | for x := x0; x <= x1; x++ { 108 | if visitor(Point{X: uint32(x), Y: uint32(y)}) != nil { 109 | return 110 | } 111 | 112 | err += deltaerr 113 | for err >= 0.5 { 114 | y = uint32(int(y) + signDeltaY) 115 | 116 | err -= 1.0 117 | } 118 | } 119 | } 120 | 121 | // Vector is for doing point-to-point comparisons 122 | type Vector struct { 123 | X int 124 | Y int 125 | } 126 | 127 | // Add combines two vectors 128 | func (v *Vector) Add(p Vector) Vector { 129 | return Vector{ 130 | X: v.X + p.X, 131 | Y: v.Y + p.Y} 132 | } 133 | 134 | // Magnitude returns the pythagorean theorem to a vector 135 | func (v *Vector) Magnitude() uint { 136 | return uint(math.Sqrt(math.Pow(float64(v.X), 2.0) + math.Pow(float64(v.Y), 2.0))) 137 | } 138 | 139 | // ToBytes flushes point to buffer 140 | func (p *Point) ToBytes(buf io.Writer) { 141 | binary.Write(buf, binary.LittleEndian, p) 142 | } 143 | 144 | // Bytes dumps a point into a byte array 145 | func (p *Point) Bytes() []byte { 146 | buf := new(bytes.Buffer) 147 | p.ToBytes(buf) 148 | return buf.Bytes() 149 | } 150 | 151 | // PointFromBytes rehydrates a point struct 152 | func PointFromBytes(ptBytes []byte) Point { 153 | return PointFromBuffer(bytes.NewBuffer(ptBytes)) 154 | } 155 | 156 | // PointFromBuffer pulls a point from a byte stream 157 | func PointFromBuffer(buf io.Reader) Point { 158 | var pt Point 159 | binary.Read(buf, binary.LittleEndian, &pt) 160 | return pt 161 | } 162 | 163 | // Box represents a Box, ya dingus 164 | type Box struct { 165 | TopLeft Point 166 | BottomRight Point 167 | } 168 | 169 | // BoxFromCoords returns a box from coordinates 170 | func BoxFromCoords(x1, y1, x2, y2 uint32) Box { 171 | if x2 < x1 { 172 | x1, x2 = x2, x1 173 | } 174 | 175 | if y2 < y1 { 176 | y1, y2 = y2, y1 177 | } 178 | 179 | return Box{Point{x1, y1}, Point{x2, y2}} 180 | } 181 | 182 | // BoxFromCenteraAndWidthAndHeight takes a centroid and dimensions 183 | func BoxFromCenteraAndWidthAndHeight(center *Point, width, height uint32) Box { 184 | topLeft := Point{center.X - width/2, center.Y - height/2} 185 | return Box{topLeft, Point{topLeft.X + width, topLeft.Y + height}} 186 | } 187 | 188 | // WidthAndHeight returns a width, height tuple 189 | func (b *Box) WidthAndHeight() (uint32, uint32) { 190 | return (b.BottomRight.X - b.TopLeft.X) + 1, (b.BottomRight.Y - b.TopLeft.Y) + 1 191 | } 192 | 193 | // ContainsPoint checks point membership 194 | func (b *Box) ContainsPoint(p *Point) bool { 195 | if p.X >= b.TopLeft.X && p.X <= b.BottomRight.X && p.Y >= b.TopLeft.Y && p.Y <= b.BottomRight.Y { 196 | return true 197 | } 198 | 199 | return false 200 | } 201 | 202 | // Corners return the corners of a box 203 | func (b *Box) Corners() (Point, Point, Point, Point) { 204 | return b.TopLeft, Point{b.BottomRight.X, b.TopLeft.Y}, b.BottomRight, Point{b.TopLeft.X, b.BottomRight.Y} 205 | } 206 | 207 | // Neighbor returns a box in that direction 208 | func (b *Box) Neighbor(d Direction) Box { 209 | width, height := b.WidthAndHeight() 210 | 211 | switch d { 212 | case DIRECTIONNORTH: 213 | return Box{Point{b.TopLeft.X, b.TopLeft.Y - height}, Point{b.BottomRight.X, b.BottomRight.Y - height}} 214 | case DIRECTIONEAST: 215 | return Box{Point{b.TopLeft.X + width, b.TopLeft.Y}, Point{b.BottomRight.X + width, b.BottomRight.Y}} 216 | case DIRECTIONSOUTH: 217 | return Box{Point{b.TopLeft.X, b.TopLeft.Y + height}, Point{b.BottomRight.X, b.BottomRight.Y + height}} 218 | case DIRECTIONWEST: 219 | return Box{Point{b.TopLeft.X - width, b.TopLeft.Y}, Point{b.BottomRight.X - width, b.BottomRight.Y}} 220 | } 221 | 222 | return *b 223 | } 224 | 225 | // Center returns a point on the middle of the edge, useful for doors 226 | func (b *Box) Center() Point { 227 | width, height := b.WidthAndHeight() 228 | 229 | return Point{b.TopLeft.X + width/2, b.TopLeft.Y + height/2} 230 | } 231 | 232 | // Door returns a point on the middle of the edge, useful for doors 233 | func (b *Box) Door(d Direction) Point { 234 | width, height := b.WidthAndHeight() 235 | 236 | switch d { 237 | case DIRECTIONNORTH: 238 | return Point{b.TopLeft.X + width/2, b.TopLeft.Y} 239 | case DIRECTIONEAST: 240 | return Point{b.BottomRight.X, b.TopLeft.Y + height/2} 241 | case DIRECTIONSOUTH: 242 | return Point{b.TopLeft.X + width/2, b.BottomRight.Y} 243 | case DIRECTIONWEST: 244 | return Point{b.TopLeft.X, b.TopLeft.Y + height/2} 245 | } 246 | 247 | return b.Center() 248 | } 249 | 250 | // Coordinates returns x1 y1 x2 y2 251 | func (b *Box) Coordinates() (uint32, uint32, uint32, uint32) { 252 | return b.TopLeft.X, b.TopLeft.Y, b.BottomRight.X, b.BottomRight.Y 253 | } 254 | 255 | // Direction is a cardinal direction 256 | type Direction byte 257 | 258 | // Cardinal directions 259 | const ( 260 | DIRECTIONNORTH Direction = iota 261 | DIRECTIONEAST 262 | DIRECTIONSOUTH 263 | DIRECTIONWEST 264 | ) 265 | 266 | // ToTheRight gives the direction to the right of the current one 267 | func ToTheRight(d Direction) Direction { 268 | switch d { 269 | case DIRECTIONNORTH: 270 | return DIRECTIONEAST 271 | case DIRECTIONEAST: 272 | return DIRECTIONSOUTH 273 | case DIRECTIONSOUTH: 274 | return DIRECTIONEAST 275 | case DIRECTIONWEST: 276 | return DIRECTIONNORTH 277 | } 278 | 279 | return DIRECTIONNORTH 280 | } 281 | 282 | // ToTheLeft gives the direction to the rigleftt of the current one 283 | func ToTheLeft(d Direction) Direction { 284 | switch d { 285 | case DIRECTIONNORTH: 286 | return DIRECTIONWEST 287 | case DIRECTIONWEST: 288 | return DIRECTIONSOUTH 289 | case DIRECTIONSOUTH: 290 | return DIRECTIONEAST 291 | case DIRECTIONEAST: 292 | return DIRECTIONNORTH 293 | } 294 | 295 | return DIRECTIONNORTH 296 | } 297 | 298 | // VectorForDirection maps directions to a distance vector 299 | var VectorForDirection map[Direction]Vector 300 | 301 | // DirectionForVector maps vectors to directions 302 | var DirectionForVector map[Vector]Direction 303 | 304 | // LoadResources loads data for the game 305 | func LoadResources() { 306 | loadCreatureTypes("./bestiary.json") 307 | loadItemTypes("./items.json") 308 | loadTerrainTypes("./terrain.json") 309 | } 310 | 311 | type transitionName struct { 312 | name string 313 | weight int 314 | } 315 | 316 | func makeTransitionGradient(transitionList []string) ([]transitionName, int, []string) { 317 | total := 0 318 | 319 | transitionInternalList := make([]transitionName, 0) 320 | returnTransitionList := make([]string, 0) 321 | 322 | for _, transition := range transitionList { 323 | splitString := strings.SplitN(transition, ":", 2) 324 | weightString := "1" 325 | returnTransitionList = append(returnTransitionList, splitString[0]) 326 | 327 | if (len(splitString)) > 1 { 328 | weightString = splitString[1] 329 | } 330 | 331 | weight, err := strconv.Atoi(weightString) 332 | 333 | if err != nil { 334 | panic(err) 335 | } 336 | 337 | transitionInternalList = append(transitionInternalList, transitionName{name: splitString[0], weight: weight}) 338 | total += weight 339 | } 340 | 341 | return transitionInternalList, total, returnTransitionList 342 | } 343 | 344 | // MakeGradientTransitionFunction helps build Markov chains. 345 | func MakeGradientTransitionFunction(transitionList []string) func(float64) string { 346 | transitionInternalList, total, _ := makeTransitionGradient(transitionList) 347 | 348 | return func(inNumber float64) string { 349 | endWeight := float64(total) * inNumber 350 | weight := float64(0) 351 | 352 | for _, item := range transitionInternalList { 353 | weight += float64(item.weight) 354 | 355 | if weight > endWeight { 356 | return item.name 357 | } 358 | } 359 | 360 | return transitionInternalList[len(transitionInternalList)-1].name 361 | } 362 | } 363 | 364 | // MakeTransitionFunction helps build Markov chains. 365 | func MakeTransitionFunction(name string, transitionList []string) (func() string, []string) { 366 | 367 | transitionInternalList, total, returnTransitionList := makeTransitionGradient(transitionList) 368 | 369 | return func() string { 370 | if transitionInternalList != nil && len(transitionInternalList) != 0 { 371 | weight := 0 372 | countTo := rand.Int() % total 373 | 374 | for _, item := range transitionInternalList { 375 | weight += item.weight 376 | 377 | if weight > countTo { 378 | return item.name 379 | } 380 | } 381 | } 382 | return "" 383 | }, returnTransitionList 384 | } 385 | 386 | // MSGPack packs to msgpack using JSON rules 387 | func MSGPack(target interface{}) ([]byte, error) { 388 | var outBuffer bytes.Buffer 389 | 390 | writer := msgpack.NewEncoder(&outBuffer) 391 | writer.UseJSONTag(true) 392 | err := writer.Encode(target) 393 | 394 | return outBuffer.Bytes(), err 395 | } 396 | 397 | // MSGUnpack unpacks from msgpack using JSON rules 398 | func MSGUnpack(inBytes []byte, outItem interface{}) error { 399 | var inBuffer = bytes.NewBuffer(inBytes) 400 | 401 | reader := msgpack.NewDecoder(inBuffer) 402 | reader.UseJSONTag(true) 403 | err := reader.Decode(outItem) 404 | 405 | return err 406 | } 407 | 408 | func init() { 409 | VectorForDirection = map[Direction]Vector{ 410 | DIRECTIONNORTH: Vector{X: 0, Y: -1}, 411 | DIRECTIONEAST: Vector{X: 1, Y: 0}, 412 | DIRECTIONSOUTH: Vector{X: 0, Y: 1}, 413 | DIRECTIONWEST: Vector{X: -1, Y: 0}} 414 | DirectionForVector = make(map[Vector]Direction) 415 | for k, v := range VectorForDirection { 416 | DirectionForVector[v] = k 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /terrain.json: -------------------------------------------------------------------------------- 1 | { 2 | "biomes": { 3 | "open-grass": { 4 | "Algorithm": "noise", 5 | "AlgorithmParameters": { 6 | "terrains": "clearing-grass:10;clearing-deep-grass:10;clearing-tree:15", 7 | "fill-border": "true", 8 | "spread-neighbors": "2" 9 | }, 10 | "Transitions": [ 11 | "open-grass:20", 12 | "ruins:1", 13 | "castle:1", 14 | "savannah:8" 15 | ] 16 | }, 17 | "ruins": { 18 | "Algorithm": "ruin", 19 | "AlgorithmParameters": { 20 | "cell-size": "64", 21 | "terrains": "clearing-grass:10;clearing-deep-grass:25", 22 | "floor": "ruin-floor", 23 | "wall": "ruin-wall" 24 | }, 25 | "Transitions": [ 26 | "open-grass" 27 | ] 28 | }, 29 | "castle": { 30 | "Algorithm": "castle", 31 | "AlgorithmParameters": { 32 | "cell-size": "32", 33 | "radius": "24", 34 | "terrains": "clearing-grass:10;clearing-deep-grass:25", 35 | "seed-exit": "clearing-grass", 36 | "wall-thickness": "3", 37 | "seed-entry": "castle-gravel", 38 | "wall-texture": "castle-clearing-wall", 39 | "floor": "castle-gravel" 40 | }, 41 | "Transitions": [ 42 | "open-grass" 43 | ] 44 | }, 45 | "savannah": { 46 | "Algorithm": "noise", 47 | "AlgorithmParameters": { 48 | "terrains": "open-savannah:5;savannah:15", 49 | "fill-border": "true", 50 | "spread-neighbors": "1" 51 | }, 52 | "Transitions": [ 53 | "savannah:2", 54 | "open-grass", 55 | "desert" 56 | ] 57 | }, 58 | "desert": { 59 | "Algorithm": "noise", 60 | "AlgorithmParameters": { 61 | "terrains": "widedesert:20;desert:10;desert-cactus:5;savannah:15", 62 | "fill-border": "true", 63 | "spread-neighbors": "12" 64 | }, 65 | "Transitions": [ 66 | "savannah" 67 | ] 68 | } 69 | }, 70 | "cells": { 71 | "clearing": { 72 | "Name": "Clearing of %s", 73 | "Permeable": false, 74 | "Blocking": false, 75 | "ItemDrops": [ 76 | { 77 | "Name": "Simple Sword", 78 | "Probability": 1.0 79 | }, 80 | { 81 | "Name": "Simple Bow", 82 | "Probability": 1.0 83 | }, 84 | { 85 | "Name": "Simple Wand", 86 | "Probability": 1.0 87 | } 88 | ], 89 | "FGcolor": 184, 90 | "BGcolor": 0, 91 | "Bold": false, 92 | "Representations": [ 93 | 43 94 | ] 95 | }, 96 | "clearing-grass": { 97 | "Name": "%s grasslands", 98 | "Permeable": false, 99 | "Blocking": false, 100 | "ItemDrops": [ 101 | { 102 | "Name": "Shiny Rock", 103 | "Probability": 0.02, 104 | "Cluster": 4 105 | } 106 | ], 107 | "CreatureSpawns": [ 108 | { 109 | "Name": "goat", 110 | "Probability": 0.001, 111 | "Cluster": 10 112 | } 113 | ], 114 | "FGcolor": 112, 115 | "BGcolor": 154, 116 | "Bold": false, 117 | "Representations": [ 118 | 8281, 119 | 8283, 120 | 8280, 121 | 8278 122 | ] 123 | }, 124 | "clearing-deep-grass": { 125 | "Name": "%s grasslands", 126 | "Permeable": false, 127 | "Blocking": false, 128 | "CreatureSpawns": [ 129 | { 130 | "Name": "grass-snake", 131 | "Probability": 0.01, 132 | "Cluster": 5 133 | } 134 | ], 135 | "FGcolor": 154, 136 | "BGcolor": 118, 137 | "Bold": false, 138 | "Representations": [ 139 | 8281, 140 | 8283, 141 | 8280, 142 | 8278 143 | ] 144 | }, 145 | "clearing-fairy-circle": { 146 | "Algorithm": "circle", 147 | "AlgorithmParameters": { 148 | "radius": "4", 149 | "entry-radius": "3", 150 | "seed-exit": "clearing-fairy-circle-grass", 151 | "circle-fill": "clearing-tree", 152 | "circle-thickness": "2", 153 | "center-fill": "clearing-fairy-circle-grass" 154 | } 155 | }, 156 | "clearing-large-fairy-circle": { 157 | "Algorithm": "circle", 158 | "AlgorithmParameters": { 159 | "radius": "8", 160 | "entry-radius": "8", 161 | "seed-exit": "clearing-fairy-circle-grass", 162 | "circle-fill": "clearing-tree", 163 | "circle-thickness": "3", 164 | "center-fill": "clearing-fairy-circle-grass" 165 | } 166 | }, 167 | "clearing-fairy-circle-grass": { 168 | "Name": "Fairy Circle of %s", 169 | "Permeable": false, 170 | "Blocking": false, 171 | "Transitions": [ 172 | "clearing-deep-grass" 173 | ], 174 | "Algorithm": "spread", 175 | "FGcolor": 154, 176 | "BGcolor": 118, 177 | "Bold": false, 178 | "Representations": [ 179 | 8281, 180 | 8283, 181 | 8280, 182 | 8278 183 | ] 184 | }, 185 | "clearing-tree": { 186 | "Name": "Tree in %s", 187 | "Permeable": false, 188 | "Blocking": true, 189 | "Transitions": [ 190 | "clearing-deep-grass:3", 191 | "clearing-tree" 192 | ], 193 | "Algorithm": "tendril", 194 | "AlgorithmParameters": { 195 | "radius": "2", 196 | "tendrilcount": "2" 197 | }, 198 | "FGcolor": 34, 199 | "BGcolor": 118, 200 | "Bold": false, 201 | "Representations": [ 202 | 9827, 203 | 8607, 204 | 8613 205 | ] 206 | }, 207 | "trailhead": { 208 | "Name": "%s grasslands", 209 | "Permeable": false, 210 | "Blocking": false, 211 | "Transitions": [ 212 | "trail:14", 213 | "clearing-grass:10", 214 | "!previous:10" 215 | ], 216 | "Algorithm": "once", 217 | "AlgorithmParameters": {}, 218 | "FGcolor": 144, 219 | "BGcolor": 154, 220 | "Bold": false, 221 | "Representations": [ 222 | 8284 223 | ] 224 | }, 225 | "trail": { 226 | "Name": "%s trail", 227 | "Permeable": false, 228 | "Blocking": false, 229 | "Transitions": [ 230 | "trail:20", 231 | "!previous:15", 232 | "clearing-trail-ruin:100" 233 | ], 234 | "Algorithm": "path", 235 | "AlgorithmParameters": { 236 | "path": "trail", 237 | "neighbor": "clearing-grass", 238 | "endcap": "trailhead", 239 | "radius": "10" 240 | }, 241 | "FGcolor": 144, 242 | "BGcolor": 155, 243 | "Bold": false, 244 | "Representations": [ 245 | 8280, 246 | 8281 247 | ] 248 | }, 249 | "ruin-floor": { 250 | "Name": "Ruins of %s", 251 | "Permeable": false, 252 | "Transitions": [ 253 | "clearing-trail-ruin:2", 254 | "clearing-grass:1" 255 | ], 256 | "Algorithm": "dungeon-room", 257 | "AlgorithmParameters": { 258 | "minradius": "2", 259 | "maxradius": "4", 260 | "wall": "ruin-wall", 261 | "exit": "clearing-trail-ruin", 262 | "fallback": "clearing-deep-grass" 263 | }, 264 | "CreatureSpawns": [ 265 | { 266 | "Name": "rat", 267 | "Probability": 0.1, 268 | "Cluster": 2 269 | }, 270 | { 271 | "Name": "mouse", 272 | "Probability": 0.1, 273 | "Cluster": 6 274 | } 275 | ], 276 | "FGColor": 64, 277 | "BGColor": 142, 278 | "Representations": [ 279 | 32 280 | ] 281 | }, 282 | "ruin-wall": { 283 | "Name": "Ruin Walls of %s", 284 | "Permeable": false, 285 | "FGColor": 64, 286 | "BGColor": 247, 287 | "Blocking": true, 288 | "Representations": [ 289 | 9622, 290 | 9623, 291 | 9624, 292 | 9625, 293 | 9626, 294 | 9627, 295 | 9628, 296 | 9629, 297 | 9630, 298 | 9631 299 | ] 300 | }, 301 | "savannah": { 302 | "Name": "Savannah %s", 303 | "Permeable": false, 304 | "Blocking": false, 305 | "Transitions": [ 306 | "savannah-circle:25", 307 | "savannah:20", 308 | "desert:10", 309 | "change-biomes:10" 310 | ], 311 | "Algorithm": "spread", 312 | "AlgorithmParameters": { 313 | "radius": "1" 314 | }, 315 | "FGcolor": 106, 316 | "BGcolor": 149, 317 | "Bold": false, 318 | "Representations": [ 319 | 8281, 320 | 8283, 321 | 8280, 322 | 8278, 323 | 32 324 | ] 325 | }, 326 | "open-savannah": { 327 | "Name": "Savannah %s", 328 | "Permeable": false, 329 | "Blocking": false, 330 | "Transitions": [ 331 | "savannah-circle:25", 332 | "savannah:20", 333 | "desert:10", 334 | "change-biomes:10" 335 | ], 336 | "Algorithm": "spread", 337 | "AlgorithmParameters": { 338 | "radius": "1" 339 | }, 340 | "FGcolor": 106, 341 | "BGcolor": 148, 342 | "Bold": false, 343 | "Representations": [ 344 | 8281, 345 | 8283, 346 | 8280, 347 | 8278, 348 | 32 349 | ] 350 | }, 351 | "savannah-circle": { 352 | "Name": "Savannah cactus clearing %s", 353 | "Permeable": false, 354 | "Blocking": false, 355 | "Transitions": [ 356 | "savannah-circle:25", 357 | "savannah:20", 358 | "savannah-circle-small:40", 359 | "change-biomes:20" 360 | ], 361 | "Algorithm": "circle", 362 | "AlgorithmParameters": { 363 | "radius": "6", 364 | "entry-radius": "4", 365 | "seed-exit": "savannah", 366 | "circle-fill": "savannah", 367 | "circle-thickness": "3", 368 | "center-fill": "desert-cactus" 369 | }, 370 | "FGcolor": 106, 371 | "BGcolor": 149, 372 | "Bold": false, 373 | "Representations": [ 374 | 32 375 | ] 376 | }, 377 | "savannah-circle-small": { 378 | "Name": "Savannah cactus grove %s", 379 | "Permeable": false, 380 | "Blocking": false, 381 | "Transitions": [ 382 | "savannah-circle:25", 383 | "savannah-circle-small:40", 384 | "savannah:20", 385 | "change-biomes:5" 386 | ], 387 | "Algorithm": "circle", 388 | "AlgorithmParameters": { 389 | "radius": "4", 390 | "entry-radius": "4", 391 | "seed-exit": "savannah", 392 | "circle-fill": "savannah", 393 | "circle-thickness": "1", 394 | "center-fill": "desert-cactus" 395 | }, 396 | "FGcolor": 106, 397 | "BGcolor": 149, 398 | "Bold": false, 399 | "Representations": [ 400 | 32 401 | ] 402 | }, 403 | "desert": { 404 | "Name": "%s sands", 405 | "Permeable": false, 406 | "Blocking": false, 407 | "Transitions": [ 408 | "savannah:2", 409 | "desert:1", 410 | "widedesert:2" 411 | ], 412 | "Algorithm": "spread", 413 | "AlgorithmParameters": { 414 | "radius": "4" 415 | }, 416 | "CreatureSpawns": [ 417 | { 418 | "Name": "tarantula", 419 | "Probability": 0.1, 420 | "Cluster": 2 421 | } 422 | ], 423 | "FGcolor": 220, 424 | "BGcolor": 184, 425 | "Bold": false, 426 | "Representations": [ 427 | 8764, 428 | 8765 429 | ] 430 | }, 431 | "widedesert": { 432 | "Name": "%s desert", 433 | "Permeable": false, 434 | "Blocking": false, 435 | "Transitions": [ 436 | "desert:1", 437 | "desert-cactus:10", 438 | "widedesert:10", 439 | "change-biomes:10" 440 | ], 441 | "Algorithm": "tendril", 442 | "AlgorithmParameters": { 443 | "radius": "10", 444 | "tendrilcount": "5" 445 | }, 446 | "CreatureSpawns": [ 447 | { 448 | "Name": "scorpion", 449 | "Probability": 0.01, 450 | "Cluster": 1 451 | }, 452 | { 453 | "Name": "tarantula", 454 | "Probability": 0.01, 455 | "Cluster": 5 456 | } 457 | ], 458 | "FGcolor": 220, 459 | "BGcolor": 226, 460 | "Bold": false, 461 | "Representations": [ 462 | 8764, 463 | 8765 464 | ] 465 | }, 466 | "desert-cactus": { 467 | "Name": "%s desert cactus", 468 | "Permeable": false, 469 | "Blocking": true, 470 | "Transitions": [ 471 | "widedesert:1" 472 | ], 473 | "Algorithm": "spread", 474 | "AlgorithmParameters": { 475 | "radius": "2" 476 | }, 477 | "FGcolor": 71, 478 | "BGcolor": 226, 479 | "Bold": true, 480 | "Representations": [ 481 | 968, 482 | 936, 483 | 969 484 | ] 485 | }, 486 | "castle-clearing": { 487 | "Algorithm": "great-wall", 488 | "AlgorithmParameters": { 489 | "radius": "20", 490 | "wall-thickness": "3", 491 | "wall-texture": "castle-clearing-wall", 492 | "seed-entry": "castle-gravel", 493 | "seed-exit": "clearing-deep-grass" 494 | }, 495 | "FGcolor": 15, 496 | "BGcolor": 15, 497 | "Bold": false, 498 | "Representations": [ 499 | 32 500 | ] 501 | }, 502 | "castle-clearing-wall": { 503 | "Permeable": false, 504 | "Blocking": true, 505 | "FGcolor": 242, 506 | "BGcolor": 245, 507 | "Representations": [ 508 | 9625, 509 | 9626, 510 | 9627, 511 | 9628, 512 | 9629, 513 | 9630, 514 | 9631 515 | ] 516 | }, 517 | "castle-gravel": { 518 | "Name": "Courtyard of Castle %s", 519 | "Permeable": false, 520 | "Blocking": false, 521 | "Algorithm": "spread", 522 | "AlgorithmParameters": { 523 | "radius": "20" 524 | }, 525 | "Transitions": [ 526 | "castle-gravel" 527 | ], 528 | "CreatureSpawns": [ 529 | { 530 | "Name": "skeltal", 531 | "Probability": 0.01, 532 | "Cluster": 1 533 | }, 534 | { 535 | "Name": "vagabond", 536 | "Probability": 0.01, 537 | "Cluster": 1 538 | }, 539 | { 540 | "Name": "centipede", 541 | "Probability": 0.05, 542 | "Cluster": 10 543 | } 544 | ], 545 | "ItemDrops": [ 546 | { 547 | "Name": "Simple Sword", 548 | "Probability": 0.0001 549 | }, 550 | { 551 | "Name": "Simple Bow", 552 | "Probability": 0.0001 553 | }, 554 | { 555 | "Name": "Simple Wand", 556 | "Probability": 0.0001 557 | } 558 | ], 559 | "FGcolor": 245, 560 | "BGcolor": 15, 561 | "Representations": [ 562 | 8281, 563 | 8282 564 | ] 565 | }, 566 | "change-biomes": { 567 | "Transitions": [ 568 | "desert", 569 | "clearing-grass" 570 | ], 571 | "Algorithm": "change-of-scenery", 572 | "AlgorithmParameters": { 573 | "thickness": "10", 574 | "divider-thickness": "5", 575 | "divider-edge": "mountain-short", 576 | "divider-center": "mountain-tall", 577 | "length": "300" 578 | } 579 | }, 580 | "mountain-short": { 581 | "Permeable": false, 582 | "Blocking": true, 583 | "FGcolor": 56, 584 | "BGcolor": 53, 585 | "Representations": [ 586 | 8896, 587 | 10837, 588 | 10840, 589 | 8911 590 | ] 591 | }, 592 | "mountain-tall": { 593 | "Permeable": false, 594 | "Blocking": true, 595 | "FGcolor": 56, 596 | "BGcolor": 53, 597 | "Representations": [ 598 | 923, 599 | 94, 600 | 8743 601 | ] 602 | } 603 | } 604 | } -------------------------------------------------------------------------------- /internal/screen.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | "sort" 8 | "strings" 9 | "unicode/utf8" 10 | 11 | "github.com/ahmetb/go-cursor" 12 | "github.com/gliderlabs/ssh" 13 | "github.com/mgutz/ansi" 14 | ) 15 | 16 | // Screen represents a UI screen. For now, just an SSH terminal. 17 | type Screen interface { 18 | ToggleInput() 19 | ToggleChat() 20 | ToggleCommand() 21 | InputActive() bool 22 | InCommandMode() bool 23 | HandleInputKey(string) 24 | GetChat() string 25 | ToggleInventory() 26 | InventoryActive() bool 27 | PreviousInventoryItem() 28 | NextInventoryItem() 29 | Render() 30 | Reset() 31 | } 32 | 33 | type sshScreen struct { 34 | session ssh.Session 35 | builder WorldBuilder 36 | user User 37 | screenSize ssh.Window 38 | refreshed bool 39 | colorCodeCache map[string](func(string) string) 40 | keyCodeMap map[string]func() 41 | inputActive bool 42 | inputSticky bool 43 | inputText string 44 | commandMode bool 45 | inventoryActive bool 46 | inventoryIndex int 47 | selectedCreature string 48 | } 49 | 50 | const allowMouseInputAndHideCursor string = "\x1b[?1003h\x1b[?25l" 51 | const resetScreen string = "\x1bc" 52 | const ellipsis = "…" 53 | const hpon = "◆" 54 | const hpoff = "◇" 55 | const bgcolor = 232 56 | 57 | func truncateRight(message string, width int) string { 58 | if utf8.RuneCountInString(message) < width { 59 | fmtString := fmt.Sprintf("%%-%vs", width) 60 | 61 | return fmt.Sprintf(fmtString, message) 62 | } 63 | return string([]rune(message)[0:width-1]) + ellipsis 64 | } 65 | 66 | func truncateLeft(message string, width int) string { 67 | if utf8.RuneCountInString(message) < width { 68 | fmtString := fmt.Sprintf("%%-%vs", width) 69 | 70 | return fmt.Sprintf(fmtString, message) 71 | } 72 | strLen := utf8.RuneCountInString(message) 73 | return ellipsis + string([]rune(message)[strLen-width:strLen-1]) 74 | } 75 | 76 | func justifyRight(message string, width int) string { 77 | if utf8.RuneCountInString(message) < width { 78 | fmtString := fmt.Sprintf("%%%vs", width) 79 | 80 | return fmt.Sprintf(fmtString, message) 81 | } 82 | strLen := utf8.RuneCountInString(message) 83 | return ellipsis + string([]rune(message)[strLen-width:strLen-1]) 84 | } 85 | 86 | func centerText(message, pad string, width int) string { 87 | if utf8.RuneCountInString(message) > width { 88 | return truncateRight(message, width) 89 | } 90 | leftover := width - utf8.RuneCountInString(message) 91 | left := leftover / 2 92 | right := leftover - left 93 | 94 | if pad == "" { 95 | pad = " " 96 | } 97 | 98 | leftString := "" 99 | for utf8.RuneCountInString(leftString) <= left && utf8.RuneCountInString(leftString) <= right { 100 | leftString += pad 101 | } 102 | 103 | return fmt.Sprintf("%s%s%s", string([]rune(leftString)[0:left]), message, string([]rune(leftString)[0:right])) 104 | } 105 | 106 | func groupInventory(items []*InventoryItem) (map[string]int, map[string]string, []string) { 107 | itemCount := make(map[string]int) 108 | itemID := make(map[string]string) 109 | for _, item := range items { 110 | _, ok := itemCount[item.Name] 111 | 112 | if ok { 113 | itemCount[item.Name] = itemCount[item.Name] + 1 114 | } else { 115 | itemCount[item.Name] = 1 116 | itemID[item.Name] = item.ID 117 | } 118 | } 119 | 120 | keyList := make([]string, len(itemCount)) 121 | index := 0 122 | for k := range itemCount { 123 | keyList[index] = k 124 | index++ 125 | } 126 | sort.Strings(keyList) 127 | 128 | return itemCount, itemID, keyList 129 | } 130 | 131 | // SSHString render log item for console 132 | func (item *LogItem) SSHString(width int) string { 133 | formatFunc := ansi.ColorFunc(fmt.Sprintf("255:%v", bgcolor)) 134 | boldFormatFunc := ansi.ColorFunc(fmt.Sprintf("15+b:%v", bgcolor)) 135 | systemFunc := ansi.ColorFunc(fmt.Sprintf("230+b:%v", bgcolor)) 136 | actionFunc := ansi.ColorFunc(fmt.Sprintf("247+b:%v", bgcolor)) 137 | activityFunc := ansi.ColorFunc(fmt.Sprintf("230:%v", bgcolor)) 138 | switch item.MessageType { 139 | case MESSAGECHAT: 140 | return boldFormatFunc(item.Author) + formatFunc(": "+truncateRight(item.Message, width-(2+utf8.RuneCountInString(item.Author)))) 141 | case MESSAGESYSTEM: 142 | return systemFunc(centerText(item.Message, " ", width)) 143 | case MESSAGEACTION: 144 | return actionFunc(truncateRight(item.Message, width)) 145 | case MESSAGEACTIVITY: 146 | strWidth := width 147 | message := "" 148 | if len(item.Author) > 0 { 149 | strWidth -= (utf8.RuneCountInString(item.Author)) 150 | message = activityFunc(truncateRight(item.Message, strWidth)) + boldFormatFunc(item.Author) 151 | } else { 152 | message = activityFunc(truncateRight(item.Message, width)) 153 | } 154 | return message 155 | default: 156 | truncateRight(item.Message, width) 157 | } 158 | 159 | return truncateRight(item.Message, width) 160 | } 161 | 162 | // SSHString render inventory item for console 163 | func (item *InventoryItem) SSHString(width int) string { 164 | formatFunc := ansi.ColorFunc(fmt.Sprintf("255:%v", bgcolor)) 165 | 166 | return formatFunc(truncateRight(item.Name, width)) 167 | } 168 | 169 | func (screen *sshScreen) colorFunc(color string) func(string) string { 170 | _, ok := screen.colorCodeCache[color] 171 | 172 | if !ok { 173 | screen.colorCodeCache[color] = ansi.ColorFunc(color) 174 | } 175 | 176 | return screen.colorCodeCache[color] 177 | } 178 | 179 | func (screen *sshScreen) renderMap() { 180 | interfaceTools, ok := screen.builder.(SSHInterfaceTools) 181 | 182 | if ok { 183 | location := screen.user.Location() 184 | height := uint32(screen.screenSize.Height) 185 | if height < 20 { 186 | height = 5 187 | } else { 188 | height = (height / 2) - 2 189 | } 190 | mapArray := interfaceTools.GetTerrainMap(location.X, location.Y, uint32(screen.screenSize.Width/2)-4, height) 191 | 192 | for row := range mapArray { 193 | rowText := cursor.MoveTo(2+row, 2) 194 | for _, value := range mapArray[row] { 195 | fgcolor := value.FGColor 196 | bgcolor := value.BGColor 197 | bold := value.Bold 198 | mGlyph := value.Glyph 199 | 200 | if mGlyph == 0 { 201 | mGlyph = rune('?') 202 | } 203 | 204 | var fString string 205 | if bold { 206 | fString = fmt.Sprintf("%v+b:%v", fgcolor, bgcolor) 207 | } else { 208 | fString = fmt.Sprintf("%v:%v", fgcolor, bgcolor) 209 | } 210 | 211 | rowText += screen.colorFunc(fString)(string(mGlyph)) 212 | } 213 | 214 | rowText += screen.colorFunc("clear")("") 215 | io.WriteString(screen.session, rowText) 216 | } 217 | } 218 | } 219 | 220 | func (screen *sshScreen) renderChatInput() { 221 | inputWidth := uint32(screen.screenSize.Width/2) - 2 222 | move := cursor.MoveTo(screen.screenSize.Height-1, 2) 223 | 224 | fmtString := fmt.Sprintf("%%-%vs", inputWidth-7) 225 | 226 | chatFunc := screen.colorFunc(fmt.Sprintf("231:%v", bgcolor)) 227 | chat := chatFunc("SAY▶ ") 228 | if screen.commandMode { 229 | chat = chatFunc("CMD◊ ") 230 | } 231 | if screen.InputActive() { 232 | chatFunc = screen.colorFunc(fmt.Sprintf("0+b:%v", bgcolor-1)) 233 | } 234 | 235 | fixedChat := truncateLeft(screen.inputText, int(inputWidth-7)) 236 | 237 | inputText := fmt.Sprintf("%s%s%s", move, chat, chatFunc(fmt.Sprintf(fmtString, fixedChat))) 238 | 239 | io.WriteString(screen.session, inputText) 240 | } 241 | 242 | func (screen *sshScreen) drawBox(x, y, width, height int) { 243 | color := ansi.ColorCode(fmt.Sprintf("255:%v", bgcolor)) 244 | 245 | for i := 1; i < width; i++ { 246 | io.WriteString(screen.session, fmt.Sprintf("%s%s─", cursor.MoveTo(y, x+i), color)) 247 | io.WriteString(screen.session, fmt.Sprintf("%s%s─", cursor.MoveTo(y+height, x+i), color)) 248 | } 249 | 250 | for i := 1; i < height; i++ { 251 | midString := fmt.Sprintf("%%s%%s│%%%vs│", (width - 1)) 252 | io.WriteString(screen.session, fmt.Sprintf("%s%s│", cursor.MoveTo(y+i, x), color)) 253 | io.WriteString(screen.session, fmt.Sprintf("%s%s│", cursor.MoveTo(y+i, x+width), color)) 254 | io.WriteString(screen.session, fmt.Sprintf(midString, cursor.MoveTo(y+i, x), color, " ")) 255 | } 256 | 257 | io.WriteString(screen.session, fmt.Sprintf("%s%s╭", cursor.MoveTo(y, x), color)) 258 | io.WriteString(screen.session, fmt.Sprintf("%s%s╰", cursor.MoveTo(y+height, x), color)) 259 | io.WriteString(screen.session, fmt.Sprintf("%s%s╮", cursor.MoveTo(y, x+width), color)) 260 | io.WriteString(screen.session, fmt.Sprintf("%s%s╯", cursor.MoveTo(y+height, x+width), color)) 261 | } 262 | 263 | func (screen *sshScreen) drawFill(x, y, width, height int) { 264 | color := ansi.ColorCode(fmt.Sprintf("0:%v", bgcolor)) 265 | 266 | midString := fmt.Sprintf("%%s%%s%%%vs", (width)) 267 | for i := 0; i <= height; i++ { 268 | io.WriteString(screen.session, fmt.Sprintf(midString, cursor.MoveTo(y+i, x), color, " ")) 269 | } 270 | } 271 | 272 | func (screen *sshScreen) drawProgressMeter(min, max, fgcolor, bgcolor, width uint64) string { 273 | var blink bool 274 | if min > max { 275 | min = max 276 | blink = true 277 | } 278 | proportion := float64(float64(min) / float64(max)) 279 | if math.IsNaN(proportion) { 280 | proportion = 0.0 281 | } else if proportion < 0.05 { 282 | blink = true 283 | } 284 | onWidth := uint64(float64(width) * proportion) 285 | offWidth := uint64(float64(width) * (1.0 - proportion)) 286 | 287 | onColor := screen.colorFunc(fmt.Sprintf("%v:%v", fgcolor, bgcolor)) 288 | offColor := onColor 289 | 290 | if blink { 291 | onColor = screen.colorFunc(fmt.Sprintf("%v+B:%v", fgcolor, bgcolor)) 292 | } 293 | 294 | if (onWidth + offWidth) > width { 295 | onWidth = width 296 | offWidth = 0 297 | } else if (onWidth + offWidth) < width { 298 | onWidth += width - (onWidth + offWidth) 299 | } 300 | 301 | on := "" 302 | off := "" 303 | 304 | for i := 0; i < int(onWidth); i++ { 305 | on += hpon 306 | } 307 | 308 | for i := 0; i < int(offWidth); i++ { 309 | off += hpoff 310 | } 311 | 312 | return onColor(on) + offColor(off) 313 | } 314 | 315 | func (screen *sshScreen) drawVerticalLine(x, y, height int) { 316 | color := ansi.ColorCode(fmt.Sprintf("255:%v", bgcolor)) 317 | for i := 1; i < height; i++ { 318 | io.WriteString(screen.session, fmt.Sprintf("%s%s│", cursor.MoveTo(y+i, x), color)) 319 | } 320 | 321 | io.WriteString(screen.session, fmt.Sprintf("%s%s┬", cursor.MoveTo(y, x), color)) 322 | io.WriteString(screen.session, fmt.Sprintf("%s%s┴", cursor.MoveTo(y+height, x), color)) 323 | } 324 | 325 | func (screen *sshScreen) drawHorizontalLine(x, y, width int) { 326 | color := ansi.ColorCode(fmt.Sprintf("255:%v", bgcolor)) 327 | for i := 1; i < width; i++ { 328 | io.WriteString(screen.session, fmt.Sprintf("%s%s─", cursor.MoveTo(y, x+i), color)) 329 | } 330 | 331 | io.WriteString(screen.session, fmt.Sprintf("%s%s├", cursor.MoveTo(y, x), color)) 332 | io.WriteString(screen.session, fmt.Sprintf("%s%s┤", cursor.MoveTo(y, x+width), color)) 333 | } 334 | 335 | func (screen *sshScreen) redrawBorders() { 336 | io.WriteString(screen.session, ansi.ColorCode(fmt.Sprintf("255:%v", bgcolor))) 337 | screen.drawBox(1, 1, screen.screenSize.Width-1, screen.screenSize.Height-1) 338 | screen.drawVerticalLine(screen.screenSize.Width/2-2, 1, screen.screenSize.Height) 339 | 340 | y := screen.screenSize.Height 341 | if y < 20 { 342 | y = 5 343 | } else { 344 | y = (y / 2) - 2 345 | } 346 | screen.drawHorizontalLine(1, y+2, screen.screenSize.Width/2-3) 347 | screen.drawHorizontalLine(1, screen.screenSize.Height-2, screen.screenSize.Width/2-3) 348 | } 349 | 350 | func (screen *sshScreen) renderCharacterSheet(slotKeys map[string]func()) { 351 | bgcolor := uint64(bgcolor) 352 | warning := "" 353 | key := 'A' 354 | if float32(screen.user.HP()) < float32(screen.user.MaxHP())*.25 { 355 | bgcolor = 124 356 | warning = " (Health low) " 357 | } else if float32(screen.user.HP()) < float32(screen.user.MaxHP())*.1 { 358 | bgcolor = 160 359 | warning = " (Health CRITICAL) " 360 | } 361 | 362 | x := screen.screenSize.Width/2 - 1 363 | width := (screen.screenSize.Width - x) 364 | fmtFunc := screen.colorFunc(fmt.Sprintf("255:%v", bgcolor)) 365 | boldFunc := screen.colorFunc(fmt.Sprintf("255+bu:%v", bgcolor)) 366 | pos := screen.user.Location() 367 | 368 | CRnumberColor := screen.colorFunc(fmt.Sprintf("%v:255", bgcolor)) 369 | CRitemColor := screen.colorFunc(fmt.Sprintf("255:%v", bgcolor)) 370 | CRhiliteColor := screen.colorFunc(fmt.Sprintf("%v+b:255", bgcolor)) 371 | 372 | charge, maxcharge := screen.user.Charge() 373 | 374 | infoLines := []string{ 375 | centerText(fmt.Sprintf("%v the %v", screen.user.Username(), screen.user.Title()), " ", width), 376 | centerText(warning, "─", width), 377 | truncateRight(fmt.Sprintf("%s (%v, %v)", screen.user.LocationName(), pos.X, pos.Y), width), 378 | truncateRight(fmt.Sprintf("Charge: %v/%v", charge, maxcharge), width), 379 | screen.drawProgressMeter(screen.user.HP(), screen.user.MaxHP(), 196, bgcolor, 10) + fmtFunc(truncateRight(fmt.Sprintf(" HP: %v/%v", screen.user.HP(), screen.user.MaxHP()), width-10)), 380 | screen.drawProgressMeter(screen.user.XP(), screen.user.XPToNextLevel(), 225, bgcolor, 10) + fmtFunc(truncateRight(fmt.Sprintf(" XP: %v/%v", screen.user.XP(), screen.user.XPToNextLevel()), width-10)), 381 | screen.drawProgressMeter(screen.user.AP(), screen.user.MaxAP(), 208, bgcolor, 10) + fmtFunc(truncateRight(fmt.Sprintf(" AP: %v/%v", screen.user.AP(), screen.user.MaxAP()), width-10)), 382 | screen.drawProgressMeter(screen.user.RP(), screen.user.MaxRP(), 117, bgcolor, 10) + fmtFunc(truncateRight(fmt.Sprintf(" RP: %v/%v", screen.user.RP(), screen.user.MaxRP()), width-10)), 383 | screen.drawProgressMeter(screen.user.MP(), screen.user.MaxMP(), 76, bgcolor, 10) + fmtFunc(truncateRight(fmt.Sprintf(" MP: %v/%v", screen.user.MP(), screen.user.MaxMP()), width-10))} 384 | 385 | equipment := screen.user.Equipped() 386 | 387 | if equipment != nil && len(equipment) > 0 { 388 | extraLines := []string{centerText(" Equipment ", "─", width)} 389 | 390 | for _, item := range equipment { 391 | slotCaption := item.Name 392 | 393 | keyItem := rune(0) 394 | if slotKeys != nil { 395 | slotKey, ok := slotKeys[item.Name] 396 | if ok && slotKey != nil { 397 | if key <= 'Z' { 398 | keyItem = key 399 | screen.keyCodeMap[string(keyItem)] = slotKey 400 | key++ 401 | } 402 | } 403 | } 404 | 405 | slotString := boldFunc(truncateRight(slotCaption, width)) 406 | itemString := fmtFunc(" ") 407 | if keyItem > 0 { 408 | itemString = CRhiliteColor(fmt.Sprintf("%v ", string(keyItem))) 409 | } 410 | 411 | if item.Item != nil { 412 | itemString += fmtFunc( 413 | truncateRight( 414 | fmt.Sprintf("%v (%v)", 415 | item.Item.Name, 416 | item.Item.Type), 417 | width-2)) 418 | } else { 419 | itemString += fmtFunc(centerText("-none-", " ", width-2)) 420 | } 421 | extraLines = append(extraLines, slotString, itemString) 422 | } 423 | 424 | infoLines = append(infoLines, extraLines...) 425 | } 426 | 427 | foundSelectedCreature := false 428 | hasCreatures := false 429 | firstID := "" 430 | cell := screen.builder.World().Cell(pos.X, pos.Y) 431 | creatures := cell.GetCreatures() 432 | var selectedCreatureItem *Creature 433 | if creatures != nil && len(creatures) > 0 { 434 | hasCreatures = true 435 | extraLines := []string{centerText(" Creatures ", "─", width)} 436 | 437 | for keyIndex, creature := range creatures { 438 | labelColumn := CRitemColor(" ") 439 | 440 | labelColumn = fmt.Sprintf("%2v", keyIndex+1) 441 | cid := creature.ID 442 | if creature.HP <= 0 { 443 | labelColumn = "✘✘" 444 | } else if keyIndex < 10 { 445 | screen.keyCodeMap[fmt.Sprintf("%v", keyIndex+1)] = func() { 446 | screen.selectedCreature = cid 447 | } 448 | 449 | if firstID == "" { 450 | firstID = creature.ID 451 | } 452 | } 453 | 454 | chargeMeter := screen.drawProgressMeter(uint64(creature.Charge), uint64(creature.maxCharge), 73, bgcolor, 10) 455 | 456 | nameColumn := truncateRight(fmt.Sprintf("%s (%v/%v) AP:%v RP:%v MP:%v", 457 | creature.CreatureTypeStruct.Name, 458 | creature.HP, 459 | creature.CreatureTypeStruct.MaxHP, 460 | creature.AP, 461 | creature.RP, 462 | creature.MP), width-13) + chargeMeter 463 | 464 | if screen.selectedCreature == creature.ID && creature.HP > 0 { 465 | labelColumn = CRhiliteColor(labelColumn) 466 | nameColumn = CRhiliteColor("▸" + nameColumn) 467 | foundSelectedCreature = true 468 | selectedCreatureItem = creature 469 | } else { 470 | labelColumn = CRnumberColor(labelColumn) 471 | nameColumn = CRitemColor(" " + nameColumn) 472 | } 473 | 474 | newLine := labelColumn + nameColumn 475 | extraLines = append(extraLines, newLine) 476 | } 477 | 478 | infoLines = append(infoLines, extraLines...) 479 | } 480 | 481 | // Unselect creature if it's not here 482 | if !foundSelectedCreature { 483 | if screen.selectedCreature != "" { 484 | screen.selectedCreature = firstID 485 | } 486 | } 487 | 488 | if hasCreatures { 489 | attacks := screen.user.Attacks() 490 | if attacks != nil && len(attacks) > 0 { 491 | extraLines := []string{centerText(" Attacks ", "─", width)} 492 | 493 | for _, attack := range attacks { 494 | attackkey := " " 495 | if key <= 'Z' { 496 | if selectedCreatureItem == nil { 497 | attackkey = "◊◊" 498 | } else { 499 | keyString := string(key) 500 | attackkey = fmt.Sprintf(" %v", keyString) 501 | 502 | selc := selectedCreatureItem 503 | sela := *attack.Attack 504 | screen.keyCodeMap[keyString] = func() { 505 | selattack := screen.user.MusterAttack(sela.Name) 506 | if selattack != nil { 507 | formatString := fmt.Sprintf("Attacking %v with %v", selc.CreatureTypeStruct.Name, sela.Name) 508 | screen.user.Log(LogItem{Message: formatString, 509 | MessageType: MESSAGEACTION}) 510 | screen.builder.Attack(screen.user, selc, selattack) 511 | } 512 | } 513 | } 514 | } 515 | attackName := fmtFunc(truncateRight(" "+attack.Attack.String(), width-12)) 516 | 517 | if attack.Charged { 518 | attackkey = CRnumberColor(attackkey) 519 | } 520 | 521 | extraLines = append(extraLines, attackkey+attackName+screen.drawProgressMeter(uint64(charge), uint64(attack.Attack.Charge), 73, bgcolor, 10)) 522 | 523 | key++ 524 | } 525 | 526 | infoLines = append(infoLines, extraLines...) 527 | } 528 | } 529 | 530 | items := cell.InventoryItems() 531 | if items != nil && len(items) > 0 { 532 | extraLines := []string{centerText(" Items ", "─", width)} 533 | 534 | itemCount, itemID, keyList := groupInventory(items) 535 | 536 | for _, item := range keyList { 537 | itemKey := " " 538 | ID := itemID[item] 539 | 540 | if key < 'Z' { 541 | itemKey = fmt.Sprintf(" %v", string(key)) 542 | user := screen.user 543 | 544 | screen.keyCodeMap[string(key)] = func() { 545 | item := cell.PullInventoryItem(ID) 546 | if item != nil { 547 | if user.AddInventoryItem(item) == false { 548 | cell.AddInventoryItem(item) 549 | } else { 550 | user.AddInventoryItem(item) 551 | } 552 | } 553 | } 554 | 555 | key++ 556 | } 557 | 558 | countLine := fmt.Sprintf("x%v", itemCount[item]) 559 | itemString := CRnumberColor(itemKey) + fmtFunc(truncateRight(" "+item, width-(2+utf8.RuneCountInString(countLine)))+fmtFunc(countLine)) 560 | extraLines = append(extraLines, itemString) 561 | } 562 | 563 | infoLines = append(infoLines, extraLines...) 564 | } 565 | 566 | infoLines = append(infoLines, centerText(" ❦ ", "─", width)) 567 | 568 | for index, line := range infoLines { 569 | io.WriteString(screen.session, fmt.Sprintf("%s%s", cursor.MoveTo(2+index, x), fmtFunc(line))) 570 | if index+2 > int(screen.screenSize.Height) { 571 | break 572 | } 573 | } 574 | 575 | lastLine := len(infoLines) + 1 576 | screen.drawFill(x, lastLine+1, width, screen.screenSize.Height-(lastLine+2)) 577 | } 578 | 579 | func (screen *sshScreen) renderInventory() map[string]func() { 580 | slotCodeMap := make(map[string]func()) 581 | fmtFunc := screen.colorFunc(fmt.Sprintf("255:%v", bgcolor)) 582 | selectColor := screen.colorFunc(fmt.Sprintf("%v+b:255", bgcolor)) 583 | keyFunc := screen.colorFunc(fmt.Sprintf("255+b:%v", bgcolor)) 584 | 585 | y := screen.screenSize.Height 586 | if y < 20 { 587 | y = 5 588 | } else { 589 | y = (y / 2) - 2 590 | } 591 | 592 | screenX := 2 593 | screenWidth := screen.screenSize.Width/2 - 3 594 | 595 | itemCount, itemID, keyList := groupInventory(screen.user.InventoryItems()) 596 | 597 | if screen.inventoryIndex >= len(keyList) { 598 | screen.inventoryIndex = 0 599 | } else if screen.inventoryIndex < 0 { 600 | screen.inventoryIndex = len(keyList) - 1 601 | } 602 | 603 | row := y + 3 604 | height := screen.screenSize.Height - 4 - row 605 | offset := screen.inventoryIndex - height/2 606 | if offset < 0 { 607 | offset = 0 608 | } else if offset >= len(keyList)-height { 609 | offset = len(keyList) - height - 1 610 | } 611 | ShowLines: 612 | for index, itemName := range keyList[offset:len(keyList)] { 613 | move := cursor.MoveTo(row, screenX) 614 | 615 | lString := fmt.Sprintf("x%v", itemCount[itemName]) 616 | fString := truncateRight(itemName, screenWidth-1-(utf8.RuneCountInString(lString))) 617 | var lineString string 618 | 619 | if screen.inventoryIndex == index+offset { 620 | lineString = selectColor(fString + lString) 621 | user := screen.user 622 | itemIDToGet := itemID[itemName] 623 | 624 | item := user.InventoryItem(itemIDToGet) 625 | if item != nil { 626 | newItem := item 627 | 628 | for _, slot := range user.EquippableSlots(item) { 629 | currentUser := user 630 | slotName := slot 631 | slotCodeMap[slotName] = func() { 632 | pulledItem := currentUser.PullInventoryItem(newItem.ID) 633 | if pulledItem != nil { 634 | unequppedItem, err := currentUser.Equip(slotName, pulledItem) 635 | if unequppedItem != nil { 636 | if !currentUser.AddInventoryItem(unequppedItem) { 637 | currentUser.Cell().AddInventoryItem(unequppedItem) 638 | } 639 | } 640 | 641 | if err != nil { 642 | user.Log(LogItem{MessageType: MESSAGEACTIVITY, Message: err.Error()}) 643 | } 644 | } 645 | 646 | screen.Render() 647 | } 648 | } 649 | } 650 | 651 | screen.keyCodeMap["{"] = func() { 652 | location := user.Location() 653 | item := user.PullInventoryItem(itemIDToGet) 654 | if item != nil { 655 | if !screen.builder.World().Cell(location.X, location.Y).AddInventoryItem(item) { 656 | user.AddInventoryItem(item) 657 | } 658 | } 659 | } 660 | } else { 661 | lineString = fmtFunc(fString + lString) 662 | } 663 | 664 | io.WriteString(screen.session, move+fmtFunc(lineString)) 665 | 666 | row++ 667 | if row > screen.screenSize.Height-4 { 668 | break ShowLines 669 | } 670 | } 671 | 672 | screen.drawFill(screenX, row, screenWidth-1, screen.screenSize.Height-4-row) 673 | io.WriteString(screen.session, 674 | cursor.MoveTo(screen.screenSize.Height-3, screenX)+ 675 | keyFunc( 676 | justifyRight( 677 | "[: Prev ]: Next {: Drop", 678 | screenWidth-1))) 679 | 680 | return slotCodeMap 681 | } 682 | 683 | func (screen *sshScreen) renderLog() { 684 | y := screen.screenSize.Height 685 | if y < 20 { 686 | y = 5 687 | } else { 688 | y = (y / 2) - 2 689 | } 690 | 691 | screenX := 2 692 | screenWidth := screen.screenSize.Width/2 - 3 693 | log := screen.user.GetLog() 694 | row := screen.screenSize.Height - 3 695 | 696 | for _, item := range log { 697 | move := cursor.MoveTo(row, screenX) 698 | io.WriteString(screen.session, move+item.SSHString(screenWidth-1)) 699 | row-- 700 | 701 | if row < y+3 { 702 | return 703 | } 704 | } 705 | } 706 | 707 | func (screen *sshScreen) ToggleInput() { 708 | screen.inputActive = !screen.inputActive 709 | screen.inputSticky = true 710 | screen.Render() 711 | } 712 | 713 | func (screen *sshScreen) ToggleChat() { 714 | screen.inputActive = !screen.inputActive 715 | screen.inputSticky = false 716 | screen.commandMode = false 717 | screen.Render() 718 | } 719 | 720 | func (screen *sshScreen) ToggleCommand() { 721 | screen.commandMode = true 722 | if screen.inputActive { 723 | screen.HandleInputKey("/") 724 | } 725 | screen.inputActive = true 726 | screen.Render() 727 | } 728 | 729 | func (screen *sshScreen) InputActive() bool { 730 | return screen.inputActive 731 | } 732 | 733 | func (screen *sshScreen) InCommandMode() bool { 734 | return screen.commandMode 735 | } 736 | 737 | func (screen *sshScreen) HandleInputKey(input string) { 738 | if screen.inputActive { 739 | if screen.inputText == "" { 740 | if input == "/" { 741 | screen.commandMode = true 742 | screen.inputText = "" 743 | input = "" 744 | } else if input == "!" { 745 | screen.commandMode = false 746 | screen.inputText = "" 747 | input = "" 748 | } 749 | } 750 | } 751 | 752 | if !screen.inputActive { 753 | input := strings.ToUpper(input) 754 | if input == "T" || input == "!" { 755 | screen.inputActive = true 756 | } else if screen.keyCodeMap != nil { 757 | fn, ok := screen.keyCodeMap[input] 758 | if ok { 759 | fn() 760 | } 761 | } 762 | } else { 763 | if input == "BACKSPACE" { 764 | if utf8.RuneCountInString(screen.inputText) > 0 { 765 | screen.inputText = string([]rune(screen.inputText)[0 : utf8.RuneCountInString(screen.inputText)-1]) 766 | } 767 | } else if utf8.RuneCountInString(input) == 1 { 768 | screen.inputText += input 769 | } 770 | } 771 | 772 | screen.Render() 773 | } 774 | 775 | func (screen *sshScreen) GetChat() string { 776 | ct := screen.inputText 777 | screen.inputText = "" 778 | screen.inputActive = screen.inputSticky 779 | return ct 780 | } 781 | 782 | func (screen *sshScreen) ToggleInventory() { 783 | screen.inventoryActive = !screen.inventoryActive 784 | screen.refreshed = false 785 | screen.Render() 786 | } 787 | 788 | func (screen *sshScreen) InventoryActive() bool { 789 | return screen.inventoryActive 790 | } 791 | 792 | func (screen *sshScreen) PreviousInventoryItem() { 793 | screen.inventoryIndex-- 794 | screen.Render() 795 | } 796 | 797 | func (screen *sshScreen) NextInventoryItem() { 798 | screen.inventoryIndex++ 799 | screen.Render() 800 | } 801 | 802 | func (screen *sshScreen) Render() { 803 | screen.keyCodeMap = make(map[string]func()) 804 | 805 | screen.user.Reload() 806 | 807 | if screen.screenSize.Height < 20 || screen.screenSize.Width < 60 { 808 | clear := cursor.ClearEntireScreen() 809 | move := cursor.MoveTo(1, 1) 810 | io.WriteString(screen.session, 811 | fmt.Sprintf("%s%sScreen is too small. Make your terminal larger. (60x20 minimum)", clear, move)) 812 | return 813 | } else if screen.user.HP() == 0 { 814 | clear := cursor.ClearEntireScreen() 815 | dead := "You died. Respawning..." 816 | move := cursor.MoveTo(screen.screenSize.Height/2, screen.screenSize.Width/2-utf8.RuneCountInString(dead)/2) 817 | io.WriteString(screen.session, clear+move+dead) 818 | screen.refreshed = false 819 | return 820 | } 821 | 822 | if !screen.refreshed { 823 | clear := cursor.ClearEntireScreen() + allowMouseInputAndHideCursor 824 | io.WriteString(screen.session, clear) 825 | screen.redrawBorders() 826 | screen.refreshed = true 827 | } 828 | 829 | var slotKeys map[string]func() 830 | 831 | if screen.inventoryActive { 832 | slotKeys = screen.renderInventory() 833 | } else { 834 | screen.renderLog() 835 | } 836 | 837 | screen.renderMap() 838 | screen.renderChatInput() 839 | screen.renderCharacterSheet(slotKeys) 840 | } 841 | 842 | func (screen *sshScreen) Reset() { 843 | io.WriteString(screen.session, fmt.Sprintf("%s👋\n", resetScreen)) 844 | } 845 | 846 | func (screen *sshScreen) watchSSHScreen(resizeChan <-chan ssh.Window) { 847 | done := screen.session.Context().Done() 848 | for { 849 | select { 850 | case <-done: 851 | return 852 | case win := <-resizeChan: 853 | screen.screenSize = win 854 | screen.refreshed = false 855 | } 856 | } 857 | } 858 | 859 | // NewSSHScreen manages the window rendering for a game session 860 | func NewSSHScreen(session ssh.Session, builder WorldBuilder, user User) Screen { 861 | pty, resize, isPty := session.Pty() 862 | 863 | screen := sshScreen{ 864 | session: session, 865 | builder: builder, 866 | user: user, 867 | screenSize: pty.Window, 868 | colorCodeCache: make(map[string](func(string) string))} 869 | 870 | if isPty { 871 | go screen.watchSSHScreen(resize) 872 | } 873 | 874 | return &screen 875 | } 876 | -------------------------------------------------------------------------------- /internal/terraingeneration.go: -------------------------------------------------------------------------------- 1 | package mud 2 | 3 | import ( 4 | "log" 5 | "math" 6 | "math/rand" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ojrac/opensimplex-go" 12 | ) 13 | 14 | type tileFunc func(Cell, Cell, BiomeData, World) bool 15 | 16 | var tileGenerationAlgorithms map[string]tileFunc 17 | 18 | var seed opensimplex.Noise 19 | 20 | func getIntSetting(settings map[string]string, settingName string, defaultValue int) int { 21 | if settings != nil { 22 | value, ok := settings[settingName] 23 | 24 | if ok && len(value) > 0 { 25 | val, err := strconv.Atoi(value) 26 | 27 | if err == nil { 28 | return val 29 | } 30 | } 31 | } 32 | 33 | return defaultValue 34 | } 35 | 36 | func getStringSetting(settings map[string]string, settingName string, defaultValue string) string { 37 | if settings != nil { 38 | value, ok := settings[settingName] 39 | 40 | if ok && len(value) > 0 { 41 | return value 42 | } 43 | } 44 | 45 | return defaultValue 46 | } 47 | 48 | func getBoolSetting(settings map[string]string, settingName string, defaultValue bool) bool { 49 | if settings != nil { 50 | value, ok := settings[settingName] 51 | 52 | if ok && len(value) > 0 { 53 | val := strings.ToLower(value) 54 | 55 | if (val == "true") || (val == "1") { 56 | return true 57 | } else if (val == "false") || (val == "0") { 58 | return false 59 | } 60 | } 61 | } 62 | 63 | return defaultValue 64 | } 65 | 66 | func visitOnce(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 67 | cell := world.Cell(x2, y2) 68 | cell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 69 | } 70 | 71 | func tendril(x, y uint32, count uint64, world World, regionID uint64, cellTerrain *CellTerrain) { 72 | if count <= 0 { 73 | return 74 | } 75 | 76 | cell := world.Cell(x, y) 77 | if cell.IsEmpty() { 78 | cell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 79 | count-- 80 | } else { 81 | ci := cell.CellInfo() 82 | if ci.TerrainID != cellTerrain.ID { 83 | count-- 84 | 85 | // Can pass through this and keep on going 86 | if !ci.TerrainData.Permeable { 87 | return 88 | } 89 | } 90 | } 91 | 92 | width, height := world.GetDimensions() 93 | if x > 1 && y > 1 && x < width-2 && y < height-2 { 94 | nx, ny := x, y 95 | num := rand.Int() % 4 96 | if num%2 == 0 { 97 | nx += uint32(num - 1) 98 | } else { 99 | ny += uint32(num - 2) 100 | } 101 | tendril(nx, ny, count, world, regionID, cellTerrain) 102 | } 103 | } 104 | 105 | func visitTendril(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 106 | radius := getIntSetting(cellTerrain.AlgorithmParameters, "radius", 4) 107 | 108 | tendrilcount := getIntSetting(cellTerrain.AlgorithmParameters, "tendrilcount", radius) 109 | 110 | for i := 0; i < tendrilcount; i++ { 111 | tendril(x2, y2, uint64(radius), world, regionID, cellTerrain) 112 | } 113 | 114 | newCell := world.Cell(x2, y2) 115 | newCell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 116 | 117 | nx, ny := x2+(x2-x1), y2+(y2-y1) 118 | tCell := world.Cell(nx, ny) 119 | if tCell.IsEmpty() { 120 | tCell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 121 | } 122 | } 123 | 124 | func visitSpread(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 125 | blocked := false 126 | 127 | newCell := world.Cell(x2, y2) 128 | newCell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 129 | 130 | //radius := getIntSetting(cellTerrain.AlgorithmParameters, 1) 131 | 132 | xs, xe, ys, ye := -1, 1, -1, 1 133 | 134 | if x1 > x2 { 135 | xe = 0 136 | } else if x1 < x2 { 137 | xs = 0 138 | } 139 | 140 | if y1 > y2 { 141 | ye = 0 142 | } else if y1 < y2 { 143 | ys = 0 144 | } 145 | 146 | for xd := xs; xd <= xe; xd++ { 147 | for yd := ys; yd <= ye; yd++ { 148 | nx, ny := uint32(int(x2)+xd), uint32(int(y2)+yd) 149 | nxCell := world.Cell(nx, ny) 150 | if nxCell.IsEmpty() { 151 | nxCell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 152 | } else { 153 | blocked = true 154 | } 155 | } 156 | } 157 | 158 | if blocked { 159 | visitTendril(x1, y1, x2, y2, world, regionID, cellTerrain) 160 | } 161 | } 162 | 163 | func visitPath(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 164 | xd := int(x2) - int(x1) 165 | yd := int(y2) - int(y1) 166 | nx, ny := (int(x2)), (int(y2)) 167 | neighborTerrain, ok := cellTerrain.AlgorithmParameters["neighbor"] 168 | endcap, endok := cellTerrain.AlgorithmParameters["endcap"] 169 | radius := getIntSetting(cellTerrain.AlgorithmParameters, "radius", 5) 170 | 171 | firstCell := world.Cell(x1, y1) 172 | 173 | firstCell.SetCellInfo(&CellInfo{ 174 | TerrainID: cellTerrain.ID, 175 | RegionNameID: regionID}) 176 | 177 | if !ok { 178 | neighborCell := world.Cell(uint32(int(x1)+(xd*-2)), uint32(int(y1)+(yd*-2))) 179 | ci := neighborCell.CellInfo() 180 | if ci != nil { 181 | neighborTerrain = ci.TerrainID 182 | } 183 | } 184 | 185 | length := int(radius/2) + rand.Int()%int(radius/2) 186 | broken := false 187 | 188 | for i := 0; i < length; i++ { 189 | newCell := world.Cell(uint32(nx), uint32(ny)) 190 | newCellInfo := newCell.CellInfo() 191 | 192 | if newCellInfo == nil || newCellInfo.TerrainID == neighborTerrain { 193 | newCell.SetCellInfo(&CellInfo{ 194 | TerrainID: cellTerrain.ID, 195 | RegionNameID: regionID}) 196 | 197 | neighborLeft := world.Cell(uint32(nx+yd), uint32(ny+xd)) 198 | neightborRight := world.Cell(uint32(nx-yd), uint32(ny-xd)) 199 | 200 | if neighborLeft.IsEmpty() { 201 | neighborLeft.SetCellInfo(&CellInfo{ 202 | TerrainID: neighborTerrain, 203 | RegionNameID: regionID}) 204 | } 205 | if neightborRight.IsEmpty() { 206 | neightborRight.SetCellInfo(&CellInfo{ 207 | TerrainID: neighborTerrain, 208 | RegionNameID: regionID}) 209 | } 210 | } else { 211 | broken = true 212 | break 213 | } 214 | 215 | // Make trails jitter a little 216 | if rand.Int()%3 == 0 { 217 | if rand.Int()%2 == 0 { 218 | nx -= yd 219 | ny -= xd 220 | } else { 221 | nx += yd 222 | ny += xd 223 | } 224 | } else { 225 | nx += xd 226 | ny += yd 227 | } 228 | } 229 | 230 | if !broken && endok { 231 | newCell := world.Cell(uint32(nx), uint32(ny)) 232 | 233 | if newCell.IsEmpty() { 234 | newCell.SetCellInfo(&CellInfo{TerrainID: endcap, RegionNameID: regionID}) 235 | 236 | if rand.Int()%3 > 0 { 237 | visitPath(uint32(nx), uint32(ny), uint32(nx+1), uint32(ny), world, regionID, cellTerrain) 238 | visitPath(uint32(nx), uint32(ny), uint32(nx-1), uint32(ny), world, regionID, cellTerrain) 239 | visitPath(uint32(nx), uint32(ny+1), uint32(nx), uint32(ny), world, regionID, cellTerrain) 240 | visitPath(uint32(nx), uint32(ny-1), uint32(nx), uint32(ny), world, regionID, cellTerrain) 241 | } 242 | } 243 | } 244 | } 245 | 246 | func getBox(x, y uint32, direction Direction, world World, width, height uint32) (uint32, uint32, uint32, uint32, bool) { 247 | x1, y1, x2, y2 := x, y, x, y 248 | free := true 249 | 250 | switch direction { 251 | case DIRECTIONNORTH: 252 | x1, y1, x2, y2 = x-(width/2), y-(height-1), x-(width/2)+(width-1), y 253 | case DIRECTIONSOUTH: 254 | x1, y1, x2, y2 = x-(width/2), y, x-(width/2)+(width-1), y+(height-1) 255 | case DIRECTIONEAST: 256 | x1, y1, x2, y2 = x, y-(width/2), x+(height-1), y-(width/2)+(width-1) 257 | case DIRECTIONWEST: 258 | x1, y1, x2, y2 = x-(height-1), y-(width/2), x, y-(width/2)+(width-1) 259 | } 260 | 261 | BlockCheck: 262 | for xc := x1; xc <= x2; xc++ { 263 | for yc := y1; yc <= y2; yc++ { 264 | if !world.Cell(uint32(xc), uint32(yc)).IsEmpty() { 265 | free = false 266 | break BlockCheck 267 | } 268 | } 269 | } 270 | 271 | return x1, y1, x2, y2, free 272 | } 273 | 274 | func getAvailableBox(x1, y1, x2, y2 uint32, world World, height, width int) (int, int, int, int, int, int, bool) { 275 | xd := int(x2) - int(x1) 276 | yd := int(y2) - int(y1) 277 | 278 | ux, lx, uy, ly := int(x2), int(x2), int(y2), int(y2) 279 | 280 | if yd == 0 { 281 | ly -= int(height / 2) 282 | uy += int(height / 2) 283 | 284 | if xd > 0 { 285 | ux += int(width) 286 | } else { 287 | lx -= int(width) 288 | } 289 | } else if xd == 0 { 290 | lx -= int(width / 2) 291 | ux += int(width / 2) 292 | 293 | if yd > 0 { 294 | uy += int(height) 295 | } else { 296 | ly -= int(height) 297 | } 298 | } 299 | 300 | free := true 301 | 302 | BlockCheck: 303 | for xc := lx; xc <= ux; xc++ { 304 | for yc := ly; yc <= uy; yc++ { 305 | cell := world.Cell(uint32(xc), uint32(yc)) 306 | if !cell.IsEmpty() { 307 | free = false 308 | break BlockCheck 309 | } 310 | } 311 | } 312 | 313 | return lx, ly, ux, uy, xd, yd, free 314 | } 315 | 316 | func snapToTile(x1, y1 uint32, tileSize int) (uint32, uint32, uint32, uint32) { 317 | x, y := x1, y1 318 | x -= x % uint32(tileSize) 319 | y -= y % uint32(tileSize) 320 | 321 | return x, y, x + uint32(tileSize), y + uint32(tileSize) 322 | } 323 | 324 | func getTile(x1, y1 uint32, tileSize int, world World) (uint32, uint32, uint32, uint32, bool) { 325 | x, y := x1, y1 326 | x -= x % uint32(tileSize) 327 | y -= y % uint32(tileSize) 328 | 329 | empty := true 330 | 331 | EmptyCheck: 332 | for xa := x; xa <= x+uint32(tileSize); xa++ { 333 | for ya := y; ya <= y+uint32(tileSize); ya++ { 334 | if !(world.Cell(xa, ya).IsEmpty()) { 335 | empty = false 336 | break EmptyCheck 337 | } 338 | } 339 | } 340 | 341 | return x, y, x + uint32(tileSize-1), y + uint32(tileSize-1), empty 342 | } 343 | 344 | func visitDungeonRoom(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 345 | minRadius := getIntSetting(cellTerrain.AlgorithmParameters, "minradius", 5) 346 | maxRadius := getIntSetting(cellTerrain.AlgorithmParameters, "maxradius", 5) 347 | wall := getStringSetting(cellTerrain.AlgorithmParameters, "wall", cellTerrain.ID) 348 | exit := getStringSetting(cellTerrain.AlgorithmParameters, "exit", cellTerrain.ID) 349 | fallback := getStringSetting(cellTerrain.AlgorithmParameters, "fallback", cellTerrain.ID) 350 | 351 | radius := minRadius 352 | if (maxRadius - minRadius) > 0 { 353 | radius += rand.Int() % (maxRadius - minRadius) 354 | } 355 | 356 | lx, ly, ux, uy, xd, yd, free := getAvailableBox(x1, y1, x2, y2, world, radius*2, radius*2) 357 | 358 | fallbackCell := world.Cell(x2, y2) 359 | if !free { 360 | mnx, mny, mxx, mxy := lx, ly, ux, uy 361 | if xd == 0 { 362 | mnx-- 363 | mxx++ 364 | } else if yd == 0 { 365 | mny-- 366 | mxy++ 367 | } 368 | 369 | for x := mnx; x <= mxx; x++ { 370 | for y := mny; y <= mxy; y++ { 371 | setCell := world.Cell(uint32(x), uint32(y)) 372 | if setCell.IsEmpty() { 373 | setCell.SetCellInfo(&CellInfo{TerrainID: fallback, RegionNameID: regionID}) 374 | } 375 | } 376 | } 377 | 378 | fallbackCell.SetCellInfo(&CellInfo{TerrainID: fallback, RegionNameID: regionID}) 379 | } else { 380 | for xdd := lx; xdd <= ux; xdd++ { 381 | for ydd := ly; ydd <= uy; ydd++ { 382 | xdydCell := world.Cell(uint32(xdd), uint32(ydd)) 383 | if uint32(xdd) == x2 && uint32(ydd) == y2 { 384 | fallbackCell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 385 | } else if xdd == ux || xdd == lx || ydd == uy || ydd == ly { 386 | xdydCell.SetCellInfo(&CellInfo{TerrainID: wall, RegionNameID: regionID}) 387 | } else { 388 | xdydCell.SetCellInfo(&CellInfo{TerrainID: cellTerrain.ID, RegionNameID: regionID}) 389 | } 390 | } 391 | } 392 | 393 | for _, pt := range []Point{ 394 | Point{X: uint32(lx + (ux-lx)/2), Y: uint32(uy)}, 395 | Point{X: uint32(lx + (ux-lx)/2), Y: uint32(ly)}, 396 | Point{X: uint32(lx), Y: uint32(ly + (uy-ly)/2)}, 397 | Point{X: uint32(ux), Y: uint32(ly + (uy-ly)/2)}} { 398 | pt := world.Cell(pt.X, pt.Y) 399 | pt.SetCellInfo(&CellInfo{TerrainID: exit, RegionNameID: regionID}) 400 | } 401 | } 402 | } 403 | 404 | func visitGreatWall(castleWall Box, world World, regionID uint64, biome BiomeData) { 405 | settings := biome.AlgorithmParameters 406 | 407 | seedExit := getStringSetting(settings, "seed-exit", "clearing-grass") 408 | 409 | lx, ly, ux, uy := castleWall.Coordinates() 410 | 411 | wallThickness := uint32(getIntSetting(settings, "wall-thickness", 3)) 412 | seedEntry := getStringSetting(settings, "seed-entry", "gravel") 413 | wallTexture := getStringSetting(settings, "wall-texture", "castle-clearing-wall") 414 | wallTextureInfo := CellInfo{ 415 | TerrainID: wallTexture, 416 | RegionNameID: regionID} 417 | entryTextureInfo := CellInfo{ 418 | TerrainID: seedEntry, 419 | RegionNameID: regionID} 420 | 421 | floorType := getStringSetting(biome.AlgorithmParameters, "floor", "gravel") 422 | floor := CellInfo{ 423 | TerrainID: floorType, 424 | BiomeID: biome.ID, 425 | RegionNameID: regionID} 426 | 427 | width, height := castleWall.WidthAndHeight() 428 | center := castleWall.Center() 429 | innerBox := BoxFromCenteraAndWidthAndHeight(¢er, width-3, height-3) 430 | 431 | fillBox(innerBox, world, &floor) 432 | 433 | // Outline 434 | for x := uint32(0); x < (ux - lx); x++ { 435 | if rand.Int()%2 == 0 { 436 | world.Cell(uint32(lx+x), uint32(uy)).SetCellInfo(&wallTextureInfo) 437 | world.Cell(uint32(ux-x), uint32(ly)).SetCellInfo(&wallTextureInfo) 438 | world.Cell(uint32(ux-x), uint32(uy-wallThickness)).SetCellInfo(&wallTextureInfo) 439 | world.Cell(uint32(lx+x), uint32(ly+wallThickness)).SetCellInfo(&wallTextureInfo) 440 | } else if x > 1 && x < (ux-lx)-1 { 441 | world.Cell(uint32(ux-x), uint32(uy-wallThickness)).SetCellInfo(&entryTextureInfo) 442 | world.Cell(uint32(lx+x), uint32(ly+wallThickness)).SetCellInfo(&entryTextureInfo) 443 | } 444 | } 445 | for y := uint32(0); y < (uy - ly); y++ { 446 | if rand.Int()%2 == 0 { 447 | world.Cell(uint32(lx), uint32(uy-y)).SetCellInfo(&wallTextureInfo) 448 | world.Cell(uint32(ux), uint32(ly+y)).SetCellInfo(&wallTextureInfo) 449 | world.Cell(uint32(lx+wallThickness), uint32(ly+y)).SetCellInfo(&wallTextureInfo) 450 | world.Cell(uint32(ux-wallThickness), uint32(uy-y)).SetCellInfo(&wallTextureInfo) 451 | } else if y > 1 && y < (uy-ly)-1 { 452 | world.Cell(uint32(lx+wallThickness), uint32(ly+y)).SetCellInfo(&entryTextureInfo) 453 | world.Cell(uint32(ux-wallThickness), uint32(uy-y)).SetCellInfo(&entryTextureInfo) 454 | } 455 | } 456 | // Thick wall part 457 | for thickness := uint32(1); thickness < wallThickness; thickness++ { 458 | for x := lx + thickness; x <= ux-thickness; x++ { 459 | world.Cell(uint32(x), uint32(ly+thickness)).SetCellInfo(&wallTextureInfo) 460 | world.Cell(uint32(x), uint32(uy-thickness)).SetCellInfo(&wallTextureInfo) 461 | } 462 | for y := ly + thickness; y <= uy-thickness; y++ { 463 | world.Cell(uint32(lx+thickness), uint32(y)).SetCellInfo(&wallTextureInfo) 464 | world.Cell(uint32(ux-thickness), uint32(y)).SetCellInfo(&wallTextureInfo) 465 | } 466 | } 467 | 468 | walkwayCell := CellInfo{ 469 | TerrainID: seedExit, 470 | RegionNameID: regionID} 471 | for i := uint32(0); i <= wallThickness; i++ { 472 | midx, midy := lx+(ux-lx)/2, ly+(uy-ly)/2 473 | if i >= wallThickness-1 { 474 | walkwayCell.TerrainID = seedEntry 475 | } 476 | 477 | world.Cell(uint32(midx), uint32(ly+i)).SetCellInfo(&walkwayCell) 478 | world.Cell(uint32(midx), uint32(uy-i)).SetCellInfo(&walkwayCell) 479 | world.Cell(uint32(lx+i), uint32(midy)).SetCellInfo(&walkwayCell) 480 | world.Cell(uint32(ux-i), uint32(midy)).SetCellInfo(&walkwayCell) 481 | } 482 | } 483 | 484 | func visitCircle(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 485 | settings := cellTerrain.AlgorithmParameters 486 | 487 | radius := getIntSetting(settings, "radius", 50) 488 | entryRadius := getIntSetting(settings, "entry-radius", radius) 489 | seedExit := getStringSetting(settings, "seed-exit", "clearing-grass") 490 | circleFill := getStringSetting(settings, "circle-fill", "clearing-grass") 491 | circleThickness := getIntSetting(settings, "circle-thickness", radius-1) 492 | centerFill := getStringSetting(settings, "center-fill", "clearing-grass") 493 | 494 | lx, ly, ux, uy, _, _, free := getAvailableBox(x1, y1, x2, y2, world, radius*2, radius*2) 495 | 496 | midx, midy := lx+(ux-lx)/2, ly+(uy-ly)/2 497 | 498 | circleFillBlock := CellInfo{ 499 | TerrainID: circleFill, 500 | RegionNameID: regionID} 501 | 502 | centerFillBlock := CellInfo{ 503 | TerrainID: centerFill, 504 | RegionNameID: regionID} 505 | 506 | if free { 507 | regionID = world.NewPlaceID() 508 | 509 | for x := lx; x <= ux; x++ { 510 | for y := ly; y <= uy; y++ { 511 | distanceFromCenter := int(math.Sqrt(math.Pow(float64(x-midx), 2.0) + math.Pow(float64(y-midy), 2.0))) 512 | if distanceFromCenter <= radius { 513 | if distanceFromCenter > (radius - circleThickness) { 514 | world.Cell(uint32(x), uint32(y)).SetCellInfo(&circleFillBlock) 515 | } else { 516 | if centerFill != "" { 517 | world.Cell(uint32(x), uint32(y)).SetCellInfo(¢erFillBlock) 518 | } else { 519 | world.Cell(uint32(x), uint32(y)).SetCellInfo(nil) 520 | } 521 | } 522 | } 523 | } 524 | } 525 | 526 | entryCell := CellInfo{ 527 | TerrainID: seedExit, 528 | RegionNameID: regionID} 529 | for rad := 0; rad < entryRadius; rad++ { 530 | world.Cell(uint32(midx), uint32(ly+rad)).SetCellInfo(&entryCell) 531 | world.Cell(uint32(midx), uint32(uy-rad)).SetCellInfo(&entryCell) 532 | world.Cell(uint32(lx+rad), uint32(midy)).SetCellInfo(&entryCell) 533 | world.Cell(uint32(ux-rad), uint32(midy)).SetCellInfo(&entryCell) 534 | } 535 | } else { 536 | var cellTerrain *CellTerrain 537 | 538 | cellInfo := world.Cell(x1, y1).CellInfo() 539 | if cellInfo != nil { 540 | cellTerrain = &cellInfo.TerrainData 541 | } else { 542 | *cellTerrain = CellTypes[seedExit] 543 | } 544 | visitSpread(x1, y1, x2, y2, world, regionID, &cellInfo.TerrainData) 545 | } 546 | } 547 | 548 | func visitChangeOfScenery(x1, y1, x2, y2 uint32, world World, regionID uint64, cellTerrain *CellTerrain) { 549 | settings := cellTerrain.AlgorithmParameters 550 | 551 | length := 100 552 | thickness := 10 553 | dividerThickness := 5 554 | dividerEdge := "clearing-grass" 555 | dividerCenter := "clearing-center" 556 | length = getIntSetting(settings, "length", length) 557 | thickness = getIntSetting(settings, "thickness", thickness) 558 | dividerThickness = getIntSetting(settings, "divider-thickness", dividerThickness) 559 | dividerEdge = getStringSetting(settings, "divider-edge", dividerEdge) 560 | dividerCenter = getStringSetting(settings, "divider-center", dividerCenter) 561 | 562 | seedExit := "" 563 | oldInfo := seedExit 564 | oldCell := world.Cell(x1, y1).CellInfo() 565 | if oldCell != nil { 566 | oldInfo = oldCell.TerrainID 567 | } 568 | 569 | width, height := thickness, length 570 | if y1 == y2 { 571 | width, height = height, width 572 | } 573 | 574 | lx, ly, _, _, xd, yd, free := getAvailableBox(x1, y1, x2, y2, world, width, height) 575 | 576 | if free { 577 | regionID := world.NewPlaceID() 578 | 579 | dividerCenterCell := CellInfo{RegionNameID: regionID, 580 | TerrainID: dividerCenter} 581 | 582 | // Draw mountain wall 583 | xp, yp := 0, 0 584 | if xd == 0 { 585 | xp = 1 586 | } else if yd == 0 { 587 | yp = 1 588 | } 589 | 590 | jitter := 0 591 | 592 | for l := 0; l < length; l++ { 593 | xc, yc := lx+(xp*l), ly+(yp*l) 594 | localthickness := thickness 595 | 596 | if l < thickness { 597 | localthickness = (l + 1) 598 | } else if l > length-thickness { 599 | localthickness = length - l 600 | } 601 | 602 | leftInfo, rightInfo := seedExit, oldInfo 603 | if xd < 0 || yd < 0 { 604 | leftInfo, rightInfo = rightInfo, leftInfo 605 | } 606 | 607 | for thick := 0; thick < localthickness; thick++ { 608 | localthick := thick - jitter 609 | 610 | if l == length/2 || l == length/4 || l == length/4*3 { 611 | if thick < dividerThickness { 612 | dividerCenterCell.TerrainID = rightInfo 613 | } else { 614 | dividerCenterCell.TerrainID = leftInfo 615 | } 616 | } else if localthick < 1 { 617 | dividerCenterCell.TerrainID = rightInfo 618 | } else if localthick == 1 || localthick == dividerThickness { 619 | dividerCenterCell.TerrainID = dividerEdge 620 | } else if localthick < dividerThickness { 621 | dividerCenterCell.TerrainID = dividerCenter 622 | } else if localthick < dividerThickness+2+rand.Int()%2 { 623 | dividerCenterCell.TerrainID = leftInfo 624 | } else { 625 | continue 626 | } 627 | world.Cell(uint32(xc+(yp*thick)), uint32(yc+(xp*thick))).SetCellInfo(÷rCenterCell) 628 | jitter += (rand.Int() % 3) - 1 629 | if jitter < 0 { 630 | jitter = 0 631 | } else if jitter > 2 { 632 | jitter = 2 633 | } 634 | } 635 | } 636 | 637 | // Draw path out 638 | pathInfo := CellInfo{ 639 | TerrainID: oldInfo, 640 | RegionNameID: regionID} 641 | pathX, pathY := int(x2), int(y2) 642 | for t := 0; t <= thickness; t++ { 643 | if t > thickness/2 { 644 | pathInfo.TerrainID = seedExit 645 | } 646 | world.Cell(uint32(pathX), uint32(pathY)).SetCellInfo(&pathInfo) 647 | pathX += xd 648 | pathY += yd 649 | } 650 | } else { 651 | var cellTerrain *CellTerrain 652 | 653 | cellInfo := world.Cell(x1, y1).CellInfo() 654 | if cellInfo != nil { 655 | cellTerrain = &cellInfo.TerrainData 656 | } else { 657 | *cellTerrain = CellTypes[seedExit] 658 | } 659 | visitSpread(x1, y1, x2, y2, world, regionID, &cellInfo.TerrainData) 660 | } 661 | } 662 | 663 | func isBoxEmpty(box Box, world World) bool { 664 | for x := box.TopLeft.X; x <= box.BottomRight.X; x++ { 665 | for y := box.TopLeft.Y; y <= box.BottomRight.Y; y++ { 666 | if !world.Cell(x, y).IsEmpty() { 667 | return false 668 | } 669 | } 670 | } 671 | 672 | return true 673 | } 674 | 675 | func fuzzBordersWithNeighbors(x1, y1, x2, y2 uint32, biome BiomeData, world World) { 676 | top, bottom := Point{X: x1, Y: y1}, Point{X: x1, Y: y2} 677 | 678 | width, height := int(x2-x1)/2, int(y2-y1)/2 679 | 680 | for xc := x1; xc <= x2; xc++ { 681 | topCell := world.CellAtPoint(top.Neighbor(DIRECTIONNORTH)) 682 | bottomCell := world.CellAtPoint(bottom.Neighbor(DIRECTIONSOUTH)) 683 | 684 | if !topCell.IsEmpty() { 685 | topInfo := topCell.CellInfo() 686 | if topInfo.BiomeID != biome.ID { 687 | topInfo.BiomeID = biome.ID 688 | pt := top 689 | 690 | widthFill := rand.Int() % height 691 | 692 | for i := 0; i < widthFill; i++ { 693 | cell := world.CellAtPoint(pt) 694 | if cell.IsEmpty() { 695 | cell.SetCellInfo(topInfo) 696 | } 697 | pt = pt.Neighbor(DIRECTIONSOUTH) 698 | } 699 | } 700 | } 701 | 702 | if !bottomCell.IsEmpty() { 703 | bottomInfo := bottomCell.CellInfo() 704 | if bottomInfo.BiomeID != biome.ID { 705 | bottomInfo.BiomeID = biome.ID 706 | pt := bottom 707 | 708 | widthFill := rand.Int() % height 709 | 710 | for i := 0; i < widthFill; i++ { 711 | cell := world.CellAtPoint(pt) 712 | if cell.IsEmpty() { 713 | cell.SetCellInfo(bottomInfo) 714 | } 715 | pt = pt.Neighbor(DIRECTIONNORTH) 716 | } 717 | } 718 | } 719 | 720 | top = top.Neighbor(DIRECTIONEAST) 721 | bottom = bottom.Neighbor(DIRECTIONEAST) 722 | } 723 | 724 | left, right := Point{X: x1, Y: y1}, Point{X: x2, Y: y1} 725 | for xc := x1; xc <= x2; xc++ { 726 | leftCell := world.CellAtPoint(left.Neighbor(DIRECTIONWEST)) 727 | rightCell := world.CellAtPoint(right.Neighbor(DIRECTIONEAST)) 728 | 729 | if !leftCell.IsEmpty() { 730 | leftInfo := leftCell.CellInfo() 731 | if leftInfo.BiomeID != biome.ID { 732 | leftInfo.BiomeID = biome.ID 733 | pt := left 734 | 735 | heightFill := rand.Int() % width 736 | 737 | for i := 0; i < heightFill; i++ { 738 | cell := world.CellAtPoint(pt) 739 | if cell.IsEmpty() { 740 | cell.SetCellInfo(leftInfo) 741 | } 742 | pt = pt.Neighbor(DIRECTIONEAST) 743 | } 744 | } 745 | } 746 | 747 | if !rightCell.IsEmpty() { 748 | rightInfo := rightCell.CellInfo() 749 | if rightInfo.BiomeID != biome.ID { 750 | rightInfo.BiomeID = biome.ID 751 | pt := right 752 | 753 | heightFill := rand.Int() % width 754 | 755 | for i := 0; i < heightFill; i++ { 756 | cell := world.CellAtPoint(pt) 757 | if cell.IsEmpty() { 758 | cell.SetCellInfo(rightInfo) 759 | } 760 | pt = pt.Neighbor(DIRECTIONWEST) 761 | } 762 | } 763 | } 764 | 765 | left = left.Neighbor(DIRECTIONSOUTH) 766 | right = right.Neighbor(DIRECTIONSOUTH) 767 | } 768 | } 769 | 770 | func drawBoxBorder(b Box, thickness uint32, world World, terrain *CellInfo) { 771 | for i := uint32(0); i < thickness; i++ { 772 | for x := b.TopLeft.X + i; x <= b.BottomRight.X-i; x++ { 773 | world.Cell(x, b.TopLeft.Y+i).SetCellInfo(terrain) 774 | world.Cell(x, b.BottomRight.Y-i).SetCellInfo(terrain) 775 | } 776 | for y := b.TopLeft.Y + (i + 1); y <= b.BottomRight.Y-(i+1); y++ { 777 | world.Cell(b.TopLeft.X+i, y).SetCellInfo(terrain) 778 | world.Cell(b.BottomRight.X-i, y).SetCellInfo(terrain) 779 | } 780 | } 781 | } 782 | 783 | func fillBox(b Box, world World, terrain *CellInfo) { 784 | for x := b.TopLeft.X; x <= b.BottomRight.X; x++ { 785 | for y := b.TopLeft.Y; y <= b.BottomRight.Y; y++ { 786 | world.Cell(x, y).SetCellInfo(terrain) 787 | } 788 | } 789 | } 790 | 791 | func fillWithNoise(x1, y1, x2, y2 uint32, biome BiomeData, terrainFunction func(float64) string, regionID uint64, world World) { 792 | for xc := x1; xc <= x2; xc++ { 793 | for yc := y1; yc <= y2; yc++ { 794 | cell := world.Cell(xc, yc) 795 | if cell.IsEmpty() { 796 | cell.SetCellInfo(&CellInfo{ 797 | TerrainID: terrainFunction( 798 | math.Abs( 799 | seed.Eval2( 800 | float64(xc)/10.0, 801 | float64(yc)/10.0))), 802 | BiomeID: biome.ID, 803 | RegionNameID: regionID}) 804 | } 805 | } 806 | } 807 | } 808 | 809 | func tilePerlin(fromCell, toCell Cell, biome BiomeData, world World) bool { 810 | cellSize := getIntSetting(biome.AlgorithmParameters, "cell-size", 8) 811 | 812 | newLoc := toCell.Location() 813 | x1, y1, x2, y2, ok := getTile(newLoc.X, newLoc.Y, cellSize, world) 814 | 815 | // Fall back to filling in 8 cells around here we can if we can't get a proper block 816 | if !ok { 817 | x1, y1, x2, y2, _ = getTile(newLoc.X, newLoc.Y, 8, world) 818 | } 819 | 820 | terrains := strings.Split(biome.AlgorithmParameters["terrains"], ";") 821 | terrainFunction := MakeGradientTransitionFunction(terrains) 822 | 823 | fuzzBordersWithNeighbors(x1, y1, x2, y2, biome, world) 824 | fillWithNoise(x1, y1, x2, y2, biome, terrainFunction, fromCell.CellInfo().RegionNameID, world) 825 | 826 | spreadIfNew := getBoolSetting(biome.AlgorithmParameters, "spread-if-new", false) 827 | spreadNeighbors := getIntSetting(biome.AlgorithmParameters, "spread-neighbors", 0) 828 | if !fromCell.IsEmpty() && ((spreadIfNew && fromCell.CellInfo().BiomeID != biome.ID) || (spreadNeighbors > 0)) { 829 | newBox := BoxFromCoords(x1, y1, x2, y2) 830 | 831 | itemArr := []Box{ 832 | newBox.Neighbor(DIRECTIONNORTH), 833 | newBox.Neighbor(DIRECTIONEAST), 834 | newBox.Neighbor(DIRECTIONSOUTH), 835 | newBox.Neighbor(DIRECTIONWEST)} 836 | 837 | directions := []Direction{DIRECTIONNORTH, DIRECTIONSOUTH, DIRECTIONEAST, DIRECTIONWEST} 838 | 839 | for spreadNeighbors >= 0 { 840 | newItems := make([]Box, 0) 841 | visitedTiles := make(map[Point]bool) 842 | 843 | for _, item := range itemArr { 844 | if isBoxEmpty(item, world) { 845 | fillWithNoise(item.TopLeft.X, item.TopLeft.Y, item.BottomRight.X, item.BottomRight.Y, biome, terrainFunction, fromCell.CellInfo().RegionNameID, world) 846 | 847 | for _, neighboritem := range rand.Perm(len(directions)) { 848 | newBox := item.Neighbor(directions[neighboritem]) 849 | 850 | if isBoxEmpty(newBox, world) { 851 | _, ok := visitedTiles[newBox.TopLeft] 852 | 853 | if !ok { 854 | newItems = append(newItems, newBox) 855 | spreadNeighbors-- 856 | visitedTiles[newBox.TopLeft] = true 857 | } 858 | } 859 | } 860 | } 861 | } 862 | 863 | itemArr = newItems 864 | if len(itemArr) == 0 { 865 | spreadNeighbors-- 866 | } 867 | } 868 | } 869 | 870 | return true 871 | } 872 | 873 | func fillyReachy(fromCell, toCell Cell, biome BiomeData, world World) (Box, bool) { 874 | cellSize := getIntSetting(biome.AlgorithmParameters, "cell-size", 64) 875 | terrains := strings.Split(biome.AlgorithmParameters["terrains"], ";") 876 | 877 | terrainFunction := MakeGradientTransitionFunction(terrains) 878 | 879 | oldLoc := fromCell.Location() 880 | newLoc := toCell.Location() 881 | direction := DirectionForVector[oldLoc.Vector(newLoc)] 882 | 883 | x1, y1, x2, y2, ok := getBox(newLoc.X, newLoc.Y, direction, world, uint32(cellSize), uint32(cellSize)) 884 | containerBox := BoxFromCoords(x1, y1, x2, y2) 885 | 886 | // Can't fill block? Fizzle out some grass. 887 | if !ok { 888 | x1, y1, x2, y2, _ = getTile(newLoc.X, newLoc.Y, 8, world) 889 | fuzzBordersWithNeighbors(x1, y1, x2, y2, biome, world) 890 | fillWithNoise(x1, y1, x2, y2, biome, terrainFunction, fromCell.CellInfo().RegionNameID, world) 891 | 892 | containerBox = BoxFromCoords(x1, y1, x2, y2) 893 | door := containerBox.Door(direction) 894 | nextDoor := door.Neighbor(direction) 895 | x1, y1, x2, y2, ok = getBox(nextDoor.X, nextDoor.Y, direction, world, uint32(cellSize), uint32(cellSize)) 896 | if ok { 897 | containerBox = BoxFromCoords(x1, y1, x2, y2) 898 | } else { 899 | return containerBox, true 900 | } 901 | } 902 | 903 | return containerBox, false 904 | } 905 | 906 | func tileRuin(fromCell, toCell Cell, biome BiomeData, world World) bool { 907 | containerBox, handled := fillyReachy(fromCell, toCell, biome, world) 908 | 909 | if handled { 910 | return true 911 | } 912 | 913 | directions := []Direction{DIRECTIONNORTH, DIRECTIONSOUTH, DIRECTIONEAST, DIRECTIONWEST} 914 | 915 | c := containerBox.Center() 916 | x1, y1, x2, y2 := containerBox.Coordinates() 917 | 918 | itemArr := []Box{BoxFromCenteraAndWidthAndHeight(&c, 4, 4)} 919 | regionName := world.NewPlaceID() 920 | 921 | floorType := getStringSetting(biome.AlgorithmParameters, "floor", DefaultCellType) 922 | wallType := getStringSetting(biome.AlgorithmParameters, "wall", DefaultCellType) 923 | roomCount := getIntSetting(biome.AlgorithmParameters, "room-count", 20) 924 | terrains := strings.Split(biome.AlgorithmParameters["terrains"], ";") 925 | 926 | terrainFunction := MakeGradientTransitionFunction(terrains) 927 | 928 | floor := CellInfo{ 929 | TerrainID: floorType, 930 | BiomeID: biome.ID, 931 | RegionNameID: regionName} 932 | 933 | wall := CellInfo{ 934 | TerrainID: wallType, 935 | BiomeID: biome.ID, 936 | RegionNameID: regionName} 937 | 938 | for roomCount >= 0 { 939 | newItems := make([]Box, 0) 940 | visitedTiles := make(map[Point]bool) 941 | 942 | for _, item := range itemArr { 943 | if isBoxEmpty(item, world) { 944 | roomCount-- 945 | fillBox(item, world, &floor) 946 | drawBoxBorder(item, 1, world, &wall) 947 | visitedTiles[item.TopLeft] = true 948 | 949 | for _, direction := range directions { 950 | door := item.Door(direction) 951 | cell := world.CellAtPoint(door.Neighbor(direction)) 952 | ci := cell.CellInfo() 953 | if roomCount < 3 || ci != nil { 954 | world.CellAtPoint(door).SetCellInfo(&floor) 955 | 956 | if ci != nil && ci.TerrainID == wallType { 957 | cell.SetCellInfo(&floor) 958 | } 959 | } 960 | } 961 | 962 | for _, neighboritem := range rand.Perm(len(directions))[0 : 1+rand.Int()%len(directions)] { 963 | newBox := item.Neighbor(directions[neighboritem]) 964 | door := item.Door(directions[neighboritem]) 965 | 966 | if isBoxEmpty(newBox, world) { 967 | _, ok := visitedTiles[newBox.TopLeft] 968 | 969 | if !ok { 970 | world.CellAtPoint(door).SetCellInfo(&floor) 971 | newItems = append(newItems, newBox) 972 | visitedTiles[newBox.TopLeft] = true 973 | } 974 | } 975 | } 976 | } 977 | } 978 | 979 | itemArr = newItems 980 | if len(itemArr) == 0 { 981 | roomCount-- 982 | } 983 | } 984 | 985 | fuzzBordersWithNeighbors(x1, y1, x2, y2, biome, world) 986 | fillWithNoise(x1, y1, x2, y2, biome, terrainFunction, regionName, world) 987 | 988 | return true 989 | } 990 | 991 | func tileCastle(fromCell, toCell Cell, biome BiomeData, world World) bool { 992 | containerBox, handled := fillyReachy(fromCell, toCell, biome, world) 993 | 994 | if handled { 995 | return true 996 | } 997 | 998 | c := containerBox.Center() 999 | x1, y1, x2, y2 := containerBox.Coordinates() 1000 | 1001 | regionName := world.NewPlaceID() 1002 | 1003 | terrains := strings.Split(biome.AlgorithmParameters["terrains"], ";") 1004 | cellSize := getIntSetting(biome.AlgorithmParameters, "cell-size", 64) 1005 | castleSize := getIntSetting(biome.AlgorithmParameters, "radius", cellSize-10) 1006 | 1007 | terrainFunction := MakeGradientTransitionFunction(terrains) 1008 | 1009 | castleWall := BoxFromCenteraAndWidthAndHeight(&c, uint32(castleSize), uint32(castleSize)) 1010 | 1011 | visitGreatWall(castleWall, world, regionName, biome) 1012 | 1013 | fuzzBordersWithNeighbors(x1, y1, x2, y2, biome, world) 1014 | fillWithNoise(x1, y1, x2, y2, biome, terrainFunction, regionName, world) 1015 | 1016 | return true 1017 | } 1018 | 1019 | // PopulateCellFromAlgorithm will generate terrain 1020 | func PopulateCellFromAlgorithm(oldPos, newPos Cell, world World) bool { 1021 | if oldPos.IsEmpty() { 1022 | return false 1023 | } 1024 | 1025 | if !newPos.IsEmpty() { 1026 | return false 1027 | } 1028 | 1029 | fixed := false 1030 | 1031 | AlgoLoop: 1032 | for i := 0; i < 25 && fixed == false; i++ { 1033 | newBiome := oldPos.CellInfo().BiomeData.GetRandomTransition() 1034 | biome, ok := BiomeTypes[newBiome] 1035 | if !ok { 1036 | biome, ok = BiomeTypes[oldPos.CellInfo().BiomeID] 1037 | 1038 | if !ok { 1039 | return false 1040 | } 1041 | } 1042 | 1043 | algo, ok := tileGenerationAlgorithms[biome.Algorithm] 1044 | if !ok { 1045 | algo = tileGenerationAlgorithms[BiomeTypes[DefaultBiomeType].Algorithm] 1046 | } 1047 | 1048 | if algo != nil { 1049 | fixed = algo(oldPos, newPos, biome, world) 1050 | if fixed { 1051 | break AlgoLoop 1052 | } 1053 | } else { 1054 | log.Printf("Nil algorithm: %v", biome.Algorithm) 1055 | } 1056 | } 1057 | 1058 | return fixed 1059 | } 1060 | 1061 | func init() { 1062 | seed = opensimplex.New(time.Now().UnixMicro()) 1063 | 1064 | tileGenerationAlgorithms = make(map[string]tileFunc) 1065 | tileGenerationAlgorithms["noise"] = tilePerlin 1066 | tileGenerationAlgorithms["ruin"] = tileRuin 1067 | tileGenerationAlgorithms["castle"] = tileCastle 1068 | } 1069 | --------------------------------------------------------------------------------