360 lines
9.9 KiB
Go
360 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.tilde.town/tildetown/town/models"
|
|
"git.tilde.town/tildetown/town/signup"
|
|
"github.com/MakeNowJust/heredoc/v2"
|
|
"github.com/gdamore/tcell/v2"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
const (
|
|
maxInputLength = 10000
|
|
logDir = "/town/var/signups/log"
|
|
)
|
|
|
|
type scene struct {
|
|
Name string
|
|
Description string
|
|
Host *character
|
|
SorryMsg string
|
|
Input *bytes.Buffer
|
|
OnAdvance func(*scene)
|
|
OnMsg func(*scene, *tview.TextView, string)
|
|
}
|
|
|
|
func newScene(name, desc, sorryMsg string, host *character, onAdvance func(*scene), onMsg func(*scene, *tview.TextView, string)) *scene {
|
|
return &scene{
|
|
Name: name,
|
|
Description: desc,
|
|
SorryMsg: sorryMsg,
|
|
Host: host,
|
|
Input: bytes.NewBuffer([]byte{}),
|
|
OnAdvance: onAdvance,
|
|
OnMsg: onMsg,
|
|
}
|
|
}
|
|
|
|
type sceneManager struct {
|
|
scenes []*scene
|
|
Current *scene
|
|
index int
|
|
output io.Writer
|
|
Save func()
|
|
}
|
|
|
|
func (m *sceneManager) Advance() bool {
|
|
if m.Current.Name == "done" {
|
|
return false
|
|
}
|
|
if m.Current.Input.Len() == 0 {
|
|
fmt.Fprintln(m.output, m.Current.Host.Say(m.Current.SorryMsg))
|
|
return false
|
|
}
|
|
m.Current.OnAdvance(m.Current)
|
|
m.index++
|
|
m.Current = m.scenes[m.index]
|
|
if m.Current.Name == "done" {
|
|
m.Save()
|
|
}
|
|
fmt.Fprintln(m.output, heredoc.Doc(`
|
|
|
|
[purple]----------[-:-:-]
|
|
|
|
`))
|
|
fmt.Fprintln(m.output, m.Current.Description)
|
|
return true
|
|
}
|
|
|
|
func newSceneManager(output io.Writer, scenes []*scene) *sceneManager {
|
|
return &sceneManager{
|
|
output: output,
|
|
scenes: scenes,
|
|
Current: scenes[0],
|
|
}
|
|
}
|
|
|
|
type character struct {
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
func newCharacter(name, description string) *character {
|
|
return &character{
|
|
Name: name,
|
|
Description: description,
|
|
}
|
|
}
|
|
|
|
func (c *character) Say(msg string) string {
|
|
verb := "says"
|
|
if c.Name == "you" {
|
|
verb = "say"
|
|
}
|
|
return fmt.Sprintf(`[-:-:b]%s %s:[-:-:-]
|
|
%s
|
|
`,
|
|
c.Name,
|
|
verb,
|
|
strings.TrimSpace(msg))
|
|
}
|
|
|
|
func main() {
|
|
logFile := path.Join(logDir, fmt.Sprintf("%d", time.Now().Unix()))
|
|
logF, err := os.Create(logFile)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
logger := log.New(logF, "", log.Ldate|log.Ltime)
|
|
|
|
db, err := signup.ConnectDB()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
err = _main(logger, db)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(3)
|
|
}
|
|
}
|
|
|
|
func _main(l *log.Logger, db *sql.DB) error {
|
|
l.Println("starting a session")
|
|
pages := tview.NewPages()
|
|
mainFlex := tview.NewFlex()
|
|
input := tview.NewTextArea()
|
|
input.SetBorder(true).SetBorderColor(tcell.ColorPaleTurquoise)
|
|
input.SetTitle("press enter to send")
|
|
input.SetMaxLength(2000)
|
|
|
|
title := tview.NewTextView()
|
|
title.SetDynamicColors(true)
|
|
title.SetTextAlign(tview.AlignCenter)
|
|
title.SetText("[purple]the tilde town sign up portal[-]")
|
|
|
|
footer := tview.NewTextView()
|
|
footer.SetText("type /help and press enter to get help. type /quit and press enter to leave.")
|
|
|
|
msgScroll := tview.NewTextView()
|
|
msgScroll.SetDynamicColors(true)
|
|
msgScroll.SetBackgroundColor(tcell.ColorBlack)
|
|
msgScroll.SetTextColor(tcell.ColorWhite)
|
|
|
|
mainFlex.SetDirection(tview.FlexRow)
|
|
mainFlex.AddItem(title, 1, -1, false)
|
|
mainFlex.AddItem(msgScroll, 0, 1, false)
|
|
mainFlex.AddItem(input, 4, -1, true)
|
|
mainFlex.AddItem(footer, 1, -1, false)
|
|
|
|
pages.AddPage("main", mainFlex, true, true)
|
|
|
|
app := tview.NewApplication()
|
|
app.SetRoot(pages, true)
|
|
|
|
player := newCharacter("you", "it's you. how are you?")
|
|
|
|
su := &models.TownSignup{ID: -1}
|
|
|
|
save := func() {
|
|
su.Created = time.Now()
|
|
err := su.Insert(db)
|
|
|
|
if err != nil {
|
|
l.Printf("failed to write to db: %s", err.Error())
|
|
l.Printf("dumping values: %#v", su)
|
|
return
|
|
}
|
|
}
|
|
|
|
scenes := []*scene{
|
|
newScene("start", heredoc.Doc(`
|
|
You open your eyes.
|
|
|
|
You're in some kind of workshop.
|
|
Wires and computers in various state of disrepair are strewn across
|
|
tables and shelves. It smells faintly of burnt cedar.
|
|
|
|
The wires and components before you slowly drag
|
|
themselves into the shape of a small humanoid.
|
|
|
|
[-:-:b]wire guy says:[-:-:-]
|
|
hello, welcome to the application for membership in tilde town.
|
|
first, please let me know what a good [-:-:b]email address[-:-:-] is for you?
|
|
|
|
just say it out loud. as many times as you need. to get it right.
|
|
|
|
once you've told me your email you can [-:-:b]/nod[-:-:-] to move on.
|
|
`),
|
|
"i'm sorry, before going further could you share an email with me?",
|
|
newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
|
|
func(s *scene) { su.Email = string(s.Input.Bytes()) },
|
|
func(s *scene, tv *tview.TextView, msg string) {
|
|
// TODO could check and see if it's email shaped and admonish if not
|
|
trimmed := strings.TrimSpace(msg)
|
|
fmt.Fprintln(tv, s.Host.Say(fmt.Sprintf("I heard '%s'. Is that right?", trimmed)))
|
|
}),
|
|
newScene("how", heredoc.Doc(`
|
|
The workshop fades away. You hear the sound of a dial up modem
|
|
in the distance.
|
|
|
|
Trees spring up out of the ground around you: birches, oaks, maples,
|
|
firs, yews, pines, cypresses complete with tiny swamps around their trunks,
|
|
junipers, redwoods, cedars, towering palms waving gently in a breeze, eucalyptus,
|
|
banyan. the smell is riotous like a canvas with all the colors splashed on. birds
|
|
start to sing.
|
|
|
|
a shrike alights on a branch in front of you.
|
|
|
|
[-:-:b]the shrike says:[-:-:-]
|
|
phweeturpff. how did you find out about the town? did anyone refer you?
|
|
|
|
just say your answer out loud. when you've said what you want, [-:-:b]/nod[-:-:-]
|
|
to continue.
|
|
`),
|
|
"phweeturpff",
|
|
newCharacter("the shrike", "a little grey bird. it has a pretty song."),
|
|
func(s *scene) { su.How = string(s.Input.Bytes()) }, nil),
|
|
newScene("what", heredoc.Doc(`
|
|
You sink down into soft mossy floor of the forest. You find yourself floating in darkness.
|
|
|
|
At the far reaches of your vision you can make out a faint neon grid. Around you
|
|
float pieces of consumer electronic appliances from 1980s Earth. A VCR approaches
|
|
you and speaks, flapping its tape slot cover with each word.
|
|
|
|
[-:-:b]the vcr says:[-:-:-]
|
|
welcome! thank you for coming this far. just two questions left. what about
|
|
tilde town interests you? what kind of stuff might you want to get up to here?
|
|
|
|
as usual, just say your answer. when you're satisfied, please [-:-:b]/nod[-:-:-]
|
|
`),
|
|
"hmm did you say something?",
|
|
newCharacter("the vcr", "a black and grey VCR from 1991"),
|
|
func(s *scene) { su.Why = string(s.Input.Bytes()) }, nil),
|
|
newScene("link", heredoc.Doc(`
|
|
You realize your eyes have been shut. You open them and, in an instant,
|
|
the neon grid and polygons are gone. You're in a convenience store. Outside
|
|
it's dark besides a single pool of light coming from a street lamp. it's illuminating
|
|
some litter and a rusty, blue 1994 pontiac grand am.
|
|
|
|
The shelves around you are stocked with products you've never heard of before like
|
|
Visible Pants, Petty Burgers, Gentle Rice, Boo Sponge, Power Banjo, Superware, Kneephones,
|
|
and Diet Coagulator. A mop is mopping the floor and turns to you.
|
|
|
|
[-:-:b]the mop says:[-:-:-]
|
|
swishy slop. last question. where online can we get to know you? do you have a personal
|
|
website or social media presence? we'll take whatever.
|
|
|
|
say some links and words out loud, you know the drill.
|
|
|
|
when you're happy you can submit this whole experience with a [-:-:b]/nod[-:-:-]
|
|
`),
|
|
"just the one last thing please",
|
|
newCharacter("the mop", "a greying mop with a wooden handle."),
|
|
func(s *scene) { su.Links = string(s.Input.Bytes()) }, nil),
|
|
newScene("done", heredoc.Doc(`
|
|
thank you for applying to tilde.town!
|
|
|
|
please be on the look out for an email from [-:-:b]root@tilde.town[-:-:-].
|
|
it's almost certain that it will end up in your spam filter, unfortunately.
|
|
|
|
every application is reviewed by a human. it can take up to 30 days for
|
|
an application to be reviewed. not every application is approved.
|
|
|
|
you can [-:-:b]/quit[-:-:-] now
|
|
|
|
ok bye have a good one~
|
|
`),
|
|
"",
|
|
newCharacter("the æther", "the very air around you"),
|
|
nil, nil),
|
|
}
|
|
|
|
sm := newSceneManager(msgScroll, scenes)
|
|
sm.Save = save
|
|
|
|
handleInput := func(msg string) {
|
|
msg = strings.TrimSpace(msg)
|
|
if msg == "" {
|
|
return
|
|
}
|
|
if strings.HasPrefix(msg, "/") {
|
|
split := strings.Split(msg, " ")
|
|
if len(split) > 0 {
|
|
msg = split[0]
|
|
}
|
|
switch strings.TrimPrefix(msg, "/") {
|
|
case "help":
|
|
fmt.Fprintln(msgScroll, sm.Current.Host.Say(`some artificial beings will guide you through applying to tilde.town.
|
|
type things, then press enter. to take an action, put a "/" before a word.
|
|
for example typing:
|
|
/nod
|
|
and pressing enter will cause you to nod. some other verbs: /quit /look`))
|
|
case "quit":
|
|
l.Println("got /quit")
|
|
app.Stop()
|
|
case "look":
|
|
fmt.Fprintln(msgScroll, "")
|
|
fmt.Fprintln(msgScroll, sm.Current.Description)
|
|
case "nod":
|
|
if !sm.Advance() {
|
|
fmt.Fprintln(msgScroll, "you nod, but nothing happens.")
|
|
fmt.Fprintln(msgScroll)
|
|
} else {
|
|
l.Println("advancing scene")
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if sm.Current.Input.Len() > maxInputLength {
|
|
fmt.Fprintln(msgScroll,
|
|
sm.Current.Host.Say(
|
|
"sorry I've heard more than I can remember :( maybe it's time to move on"))
|
|
return
|
|
}
|
|
fmt.Fprintln(msgScroll, player.Say(msg))
|
|
fmt.Fprintln(sm.Current.Input, msg)
|
|
if sm.Current.OnMsg != nil {
|
|
sm.Current.OnMsg(sm.Current, msgScroll, msg)
|
|
}
|
|
msgScroll.ScrollToEnd()
|
|
}
|
|
|
|
defer func() {
|
|
l.Println("exiting")
|
|
db.Close()
|
|
}()
|
|
|
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyEnter:
|
|
handleInput(input.GetText())
|
|
input.SetText("", false)
|
|
return nil
|
|
}
|
|
|
|
return event
|
|
})
|
|
|
|
app.SetAfterDrawFunc(func(_ tcell.Screen) {
|
|
fmt.Fprintln(msgScroll, sm.Current.Description)
|
|
app.SetAfterDrawFunc(nil)
|
|
})
|
|
|
|
return app.Run()
|
|
}
|