From 30e47e6f7f8dc90b4626fa3a206bf2143a8dd2c2 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Mon, 9 Nov 2020 05:22:06 +0000 Subject: [PATCH] login promptingggggggg --- internal/app/buffer.go | 13 +- internal/app/form.go | 35 ++++++ internal/app/game_view.go | 57 ++++----- internal/app/login.go | 46 +++++++ internal/app/menu_list.go | 37 +++--- internal/app/node.go | 6 + internal/app/state.go | 8 ++ internal/app/text.go | 41 +++++++ internal/app/ui.go | 245 +++++++++++++------------------------- internal/app/view.go | 15 ++- internal/sim/world.go | 2 +- internal/wire/client.go | 2 +- main.go | 7 +- 13 files changed, 297 insertions(+), 217 deletions(-) create mode 100644 internal/app/form.go create mode 100644 internal/app/login.go create mode 100644 internal/app/node.go create mode 100644 internal/app/state.go create mode 100644 internal/app/text.go diff --git a/internal/app/buffer.go b/internal/app/buffer.go index 0fa2441..2e9f9d2 100644 --- a/internal/app/buffer.go +++ b/internal/app/buffer.go @@ -13,8 +13,11 @@ type buffer struct { } func newBuffer(width, height int) *buffer { - b := &buffer{width: width, height: height} - b.clear() + b := &buffer{ + width: width, + height: height, + tiles: make([]tile, width*height), + } return b } @@ -43,7 +46,11 @@ func (b *buffer) writeString(s string, start math.Vec, style tcell.Style) { } } -func (b *buffer) clear() { b.tiles = make([]tile, b.width*b.height) } +func (b *buffer) clear() { + for i, _ := range b.tiles { + b.tiles[i] = tile{} + } +} func (b *buffer) blit(s tcell.Screen, offset math.Vec) { for x := 0; x < b.width; x++ { diff --git a/internal/app/form.go b/internal/app/form.go new file mode 100644 index 0000000..9e1de04 --- /dev/null +++ b/internal/app/form.go @@ -0,0 +1,35 @@ +package app + +import ( + "github.com/gdamore/tcell/v2" + "github.com/jordanorelli/astro-domu/internal/math" +) + +type form struct { + fields []textField + active int +} + +func (f *form) handleEvent(e tcell.Event) change { + switch t := e.(type) { + case *tcell.EventKey: + key := t.Key() + switch key { + case tcell.KeyEnter: + return login{name: f.fields[0].textInput.entered} + } + } + return f.fields[0].handleEvent(e) +} + +func (f *form) draw(b *buffer, _ *state) { + for i, field := range f.fields { + b.writeString(field.label, math.Vec{0, i * 2}, tcell.StyleDefault) + b.writeString(field.prompt+field.entered, math.Vec{0, i*2 + 1}, tcell.StyleDefault) + } +} + +type textField struct { + label string + textInput +} diff --git a/internal/app/game_view.go b/internal/app/game_view.go index d4d605e..3a91f1b 100644 --- a/internal/app/game_view.go +++ b/internal/app/game_view.go @@ -3,79 +3,71 @@ package app import ( "github.com/gdamore/tcell/v2" "github.com/jordanorelli/astro-domu/internal/sim" - "github.com/jordanorelli/astro-domu/internal/wire" "github.com/jordanorelli/blammo" ) type gameView struct { *blammo.Log - room *wire.Room - me *wire.Entity inFocus bool } -func (v *gameView) handleEvent(ui *UI, e tcell.Event) bool { +func (v *gameView) handleEvent(e tcell.Event) change { switch t := e.(type) { case *tcell.EventKey: key := t.Key() if key == tcell.KeyRune { switch t.Rune() { case 'w': - v.move(ui, 0, -1) + return move{0, -1} case 'a': - v.move(ui, -1, 0) + return move{-1, 0} case 's': - v.move(ui, 0, 1) + return move{0, 1} case 'd': - v.move(ui, 1, 0) + return move{1, 0} } } default: // ui.Debug("screen saw unhandled event of type %T", e) } - return true + return nil } -func (v *gameView) move(ui *UI, dx, dy int) { - // fuck lol - go ui.client.Send(sim.Move{dx, dy}) -} - -func (v *gameView) draw(b *buffer) { - v.drawHeader(b) +func (v *gameView) draw(b *buffer, st *state) { + v.drawHeader(b, st) // fill in background dots first - for x := 0; x < v.room.Width; x++ { - for y := 0; y < v.room.Height; y++ { + for x := 0; x < st.room.Width; x++ { + for y := 0; y < st.room.Height; y++ { b.set(x+1, y+2, tile{r: '·', style: tcell.StyleDefault}) } } b.set(0, 1, tile{r: '┌'}) - b.set(v.room.Width+1, 1, tile{r: '┐'}) - b.set(0, v.room.Height+2, tile{r: '└'}) - b.set(v.room.Width+1, v.room.Height+2, tile{r: '┘'}) - for x := 0; x < v.room.Width; x++ { + b.set(st.room.Width+1, 1, tile{r: '┐'}) + b.set(0, st.room.Height+2, tile{r: '└'}) + b.set(st.room.Width+1, st.room.Height+2, tile{r: '┘'}) + for x := 0; x < st.room.Width; x++ { b.set(x+1, 1, tile{r: '─'}) - b.set(x+1, v.room.Height+2, tile{r: '─'}) + b.set(x+1, st.room.Height+2, tile{r: '─'}) } - for y := 0; y < v.room.Height; y++ { + for y := 0; y < st.room.Height; y++ { b.set(0, y+2, tile{r: '│'}) - b.set(v.room.Width+1, y+2, tile{r: '│'}) + b.set(st.room.Width+1, y+2, tile{r: '│'}) } - for _, e := range v.room.Entities { + for _, e := range st.room.Entities { pos := e.Position b.set(pos.X+1, pos.Y+2, tile{r: e.Glyph, style: tcell.StyleDefault}) } } -func (v *gameView) drawHeader(b *buffer) { +func (v *gameView) drawHeader(b *buffer, st *state) { // the first row is the name of the room that we're currently in var style tcell.Style style = style.Background(tcell.NewRGBColor(64, 64, 128)) style = style.Foreground(tcell.NewRGBColor(0, 0, 0)) - runes := []rune(v.room.Name) + runes := []rune(st.room.Name) for i := 0; i < b.width; i++ { if i < len(runes) { @@ -87,3 +79,12 @@ func (v *gameView) drawHeader(b *buffer) { } func (v *gameView) setFocus(yes bool) { v.inFocus = yes } + +type move struct { + x int + y int +} + +func (m move) exec(ui *UI) { + ui.client.Send(sim.Move{m.x, m.y}) +} diff --git a/internal/app/login.go b/internal/app/login.go new file mode 100644 index 0000000..fe0eedd --- /dev/null +++ b/internal/app/login.go @@ -0,0 +1,46 @@ +package app + +import ( + "github.com/jordanorelli/astro-domu/internal/wire" +) + +type login struct { + name string +} + +func (l login) exec(ui *UI) { + ui.client = &wire.Client{ + Log: ui.Child("client"), + Host: "cdm.jordanorelli.com", + Port: 12805, + } + + n, err := ui.client.Dial() + if err != nil { + panic("unable to dial server: " + err.Error()) + } + ui.notifications = n + + res, err := ui.client.Send(wire.Login{Name: l.name}) + if err != nil { + panic("unable to login: " + err.Error()) + } + welcome := res.Body.(*wire.Welcome) + ui.Info("cool beans! a login response: %#v", welcome) + ui.state.playerName = l.name + if ui.state.room == nil { + ui.state.room = new(wire.Room) + } + p := welcome.Players[l.name] + // avi := p.Avatar + room := welcome.Rooms[p.Room] + // e := room.Entities[p.Avatar] + ui.state.room = &room + + ui.root = &node{ + view: &gameView{ + Log: ui.Child("game-view"), + }, + } + ui.Info("done logging in, we replaced the root view whaduheck") +} diff --git a/internal/app/menu_list.go b/internal/app/menu_list.go index 5b27a64..35800df 100644 --- a/internal/app/menu_list.go +++ b/internal/app/menu_list.go @@ -3,43 +3,48 @@ package app import ( "github.com/gdamore/tcell/v2" "github.com/jordanorelli/astro-domu/internal/math" - "github.com/jordanorelli/blammo" ) type menuList struct { - *blammo.Log - choices []string - selected int + choices []menuItem + highlight int } -func (m *menuList) handleEvent(ui *UI, e tcell.Event) bool { +func (m *menuList) handleEvent(e tcell.Event) change { switch t := e.(type) { case *tcell.EventKey: key := t.Key() switch key { + case tcell.KeyEnter: + return m.choices[m.highlight].onSelect case tcell.KeyDown: - m.selected = (m.selected + 1) % len(m.choices) + m.highlight = (m.highlight + 1) % len(m.choices) + return nil case tcell.KeyUp: - if m.selected == 0 { - m.selected = len(m.choices) - 1 + if m.highlight == 0 { + m.highlight = len(m.choices) - 1 } else { - m.selected-- + m.highlight-- } + return nil } - default: - // ui.Debug("screen saw unhandled event of type %T", e) } - return true + return nil } -func (m *menuList) draw(b *buffer) { +func (m *menuList) draw(b *buffer, _ *state) { for i, choice := range m.choices { - if i == m.selected { - b.writeString("▷ "+choice, math.Vec{0, i}, tcell.StyleDefault) + if i == m.highlight { + b.writeString("▷ "+choice.name, math.Vec{0, i}, tcell.StyleDefault) } else { - b.writeString(" "+choice, math.Vec{0, i}, tcell.StyleDefault) + b.writeString(" "+choice.name, math.Vec{0, i}, tcell.StyleDefault) } } } func (m *menuList) setFocus(bool) {} + +type menuItem struct { + name string + onSelect change +} diff --git a/internal/app/node.go b/internal/app/node.go new file mode 100644 index 0000000..9250a27 --- /dev/null +++ b/internal/app/node.go @@ -0,0 +1,6 @@ +package app + +type node struct { + view + children []*node +} diff --git a/internal/app/state.go b/internal/app/state.go new file mode 100644 index 0000000..158d745 --- /dev/null +++ b/internal/app/state.go @@ -0,0 +1,8 @@ +package app + +import "github.com/jordanorelli/astro-domu/internal/wire" + +type state struct { + playerName string + room *wire.Room +} diff --git a/internal/app/text.go b/internal/app/text.go new file mode 100644 index 0000000..2156521 --- /dev/null +++ b/internal/app/text.go @@ -0,0 +1,41 @@ +package app + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/jordanorelli/astro-domu/internal/math" +) + +type textInput struct { + prompt string + entered string +} + +func (t *textInput) handleEvent(e tcell.Event) change { + switch v := e.(type) { + case *tcell.EventKey: + key := v.Key() + + if key == tcell.KeyBackspace || key == tcell.KeyBackspace2 { + line := []rune(t.entered) + if len(line) > 0 { + line = line[:len(line)-1] + } + t.entered = string(line) + break + } + + if key == tcell.KeyRune { + t.entered = fmt.Sprintf("%s%c", t.entered, v.Rune()) + } + + default: + // ui.Debug("screen saw unhandled event of type %T", e) + } + return nil +} + +func (t *textInput) draw(b *buffer, _ *state) { + b.writeString(t.prompt+t.entered, math.Vec{0, 0}, tcell.StyleDefault) +} diff --git a/internal/app/ui.go b/internal/app/ui.go index b0b7bb8..41d6fa6 100644 --- a/internal/app/ui.go +++ b/internal/app/ui.go @@ -1,7 +1,7 @@ package app import ( - "fmt" + "time" "github.com/gdamore/tcell/v2" "github.com/jordanorelli/astro-domu/internal/exit" @@ -13,87 +13,58 @@ import ( type UI struct { *blammo.Log - PlayerName string - screen tcell.Screen - room *wire.Room - client *wire.Client + screen tcell.Screen + client *wire.Client + notifications <-chan wire.Response - statusBar *statusBar - gameView *gameView - testList *menuList - chatView *chatView - focussed view + state state + root *node } func (ui *UI) Run() { ui.setupTerminal() defer ui.clearTerminal() - ui.room = new(wire.Room) + ui.root = mainMenu - if err := ui.connect(); err != nil { - return - } - - res, err := ui.client.Send(wire.Login{Name: ui.PlayerName}) - if err != nil { - ui.Error("login error: %v", err) - return - } + input := make(chan tcell.Event) + go ui.pollInput(input) - welcome, ok := res.Body.(*wire.Welcome) - if !ok { - ui.Error("unexpected initial message of type %t", res.Body) - return - } - ui.Info("welcome: %v", welcome) - meta := welcome.Players[ui.PlayerName] - room := welcome.Rooms[meta.Room] - ui.room = &room - ui.gameView = &gameView{ - Log: ui.Child("game-view"), - room: &room, - me: &wire.Entity{ - ID: meta.Avatar, - Glyph: room.Entities[meta.Avatar].Glyph, - Position: room.Entities[meta.Avatar].Position, - }, - } - ui.chatView = &chatView{ - Log: ui.Child("chat-view"), - history: make([]sim.ChatMessage, 0, 32), - } - ui.statusBar = &statusBar{} - ui.testList = &menuList{ - Log: ui.Child("menu-list"), - choices: []string{"apple", "banana", "orange"}, - } - ui.focussed = ui.gameView + tick := time.Tick(time.Second / time.Duration(30)) - ui.Info("running ui") - if ui.handleUserInput() { - ui.Info("user requested close") - ui.Info("closing client") - ui.client.Close() - ui.Info("client closed") - ui.Info("finalizing screen") - } - ui.Info("run loop done, shutting down") -} + width, height := ui.screen.Size() + b := newBuffer(width, height) -func (ui *UI) connect() error { - ui.client = &wire.Client{ - Log: ui.Child("client"), - Host: "cdm.jordanorelli.com", - Port: 12805, - } + // b, prev := newBuffer(width, height), newBuffer(width, height) - c, err := ui.client.Dial() - if err != nil { - return fmt.Errorf("unable to dial server: %v", err) + for { + notify := ui.notifications + + select { + case e := <-input: + switch v := e.(type) { + case *tcell.EventKey: + key := v.Key() + if key == tcell.KeyCtrlC { + ui.Info("saw ctrl+c keyboard input, shutting down") + return + } + } + ui.Info("input event: %v", e) + wants := ui.root.handleEvent(e) + if wants != nil { + wants.exec(ui) + } + case <-tick: + b.clear() + ui.root.draw(b, &ui.state) + b.blit(ui.screen, math.Vec{0, 0}) + ui.screen.Show() + case n := <-notify: + ui.Info("notification: %v", n) + ui.handleNotification(n.Body) + } } - go ui.handleNotifications(c) - return nil } func (ui *UI) setupTerminal() { @@ -116,63 +87,45 @@ func (ui *UI) setupTerminal() { } func (ui *UI) clearTerminal() { - ui.statusBar.clearCount++ ui.screen.Clear() ui.screen.Fini() } -func (ui *UI) handleNotifications(c <-chan wire.Response) { - for n := range c { - ui.statusBar.msgCount++ - if ui.handleNotification(n.Body) { - if ui.gameView != nil { - ui.render() - } - } - } - ui.Info("notifications channel is closed so we must be done") - ui.Info("clearing and finalizing screen from notifications goroutine") - ui.statusBar.clearCount++ - ui.screen.Clear() - ui.screen.Fini() - ui.Info("screen finalized") -} - func (ui *UI) handleNotification(v wire.Value) bool { switch n := v.(type) { case *wire.Entity: - ui.room.Entities[n.ID] = *n + ui.state.room.Entities[n.ID] = *n return true case *wire.Frame: - if ui.room == nil { - ui.room = new(wire.Room) + if ui.state.room == nil { + ui.state.room = new(wire.Room) } - ui.room.Name = n.RoomName - ui.room.Rect = n.RoomSize - ui.room.Entities = n.Entities + ui.state.room.Name = n.RoomName + ui.state.room.Rect = n.RoomSize + ui.state.room.Entities = n.Entities return true case *wire.Delta: if n.RoomSize != nil { - ui.room.Rect = *n.RoomSize + ui.state.room.Rect = *n.RoomSize } if len(n.Entities) > 0 { for id, e := range n.Entities { if e != nil { - ui.room.Entities[id] = *e + ui.state.room.Entities[id] = *e } else { - delete(ui.room.Entities, id) + delete(ui.state.room.Entities, id) } } } return true case *sim.ChatMessage: - ui.chatView.history = append(ui.chatView.history, *n) + // ui.chatView.history = append(ui.chatView.history, *n) return true default: @@ -198,79 +151,47 @@ func (ui *UI) writeString(x, y int, s string, style tcell.Style) { } } -func (ui *UI) handleUserInput() bool { - ui.statusBar.clearCount++ - ui.screen.Clear() - ui.render() +func (ui *UI) pollInput(c chan tcell.Event) { + defer close(c) for { e := ui.screen.PollEvent() if e == nil { ui.Info("run loop sees nil event, breaking out") - // someone else shut us down, so return false - return false - } - - switch v := e.(type) { - case *tcell.EventKey: - key := v.Key() - if key == tcell.KeyCtrlC { - ui.Info("saw ctrl+c keyboard input, shutting down") - // we want to shut things down - return true - } - if key == tcell.KeyTab { - ui.Info("saw tab from keyboard input, switching focussed view") - ui.focussed.setFocus(false) - switch ui.focussed { - case ui.gameView: - ui.focussed = ui.chatView - case ui.chatView: - ui.focussed = ui.testList - case ui.testList: - ui.focussed = ui.gameView - } - ui.focussed.setFocus(true) - goto HANDLED - } + return } - - ui.focussed.handleEvent(ui, e) - ui.statusBar.clearCount++ - ui.screen.Clear() - ui.render() - ui.statusBar.showCount++ - ui.screen.Show() - HANDLED: + c <- e } } -func (ui *UI) render() { - width, height := ui.screen.Size() - - { - b := newBuffer(width, 1) - ui.statusBar.draw(b) - b.blit(ui.screen, math.Vec{0, 0}) - } - - gameViewHeight := math.Max((height-1)/2, 8) - { - b := newBuffer(width/2, gameViewHeight) - ui.gameView.draw(b) - b.blit(ui.screen, math.Vec{0, 1}) - } - { - b := newBuffer(width/2, gameViewHeight) - ui.testList.draw(b) - b.blit(ui.screen, math.Vec{width / 2, 1}) - } - { - b := newBuffer(width, height-gameViewHeight-1) - ui.chatView.draw(b) - b.blit(ui.screen, math.Vec{0, gameViewHeight + 1}) - } +var mainMenu = &node{ + view: &menuList{ + choices: []menuItem{ + menuItem{ + name: "join", + onSelect: changeFn(func(ui *UI) { + ui.root = joinForm + }), + }, + menuItem{ + name: "exit", + onSelect: changeFn(func(ui *UI) { + panic("this is bad programming") + }), + }, + }, + }, +} - ui.statusBar.showCount++ - ui.screen.Show() +var joinForm = &node{ + view: &form{ + fields: []textField{ + textField{ + label: "What is your name?", + textInput: textInput{ + prompt: "> ", + }, + }, + }, + }, } diff --git a/internal/app/view.go b/internal/app/view.go index 0a662a3..38540bb 100644 --- a/internal/app/view.go +++ b/internal/app/view.go @@ -5,7 +5,18 @@ import ( ) type view interface { - handleEvent(*UI, tcell.Event) bool - draw(*buffer) + handleEvent(tcell.Event) change + draw(*buffer, *state) +} + +type focusable interface { setFocus(bool) } + +type change interface { + exec(*UI) +} + +type changeFn func(*UI) + +func (f changeFn) exec(ui *UI) { f(ui) } diff --git a/internal/sim/world.go b/internal/sim/world.go index 84e1f75..d40ecc5 100644 --- a/internal/sim/world.go +++ b/internal/sim/world.go @@ -224,7 +224,7 @@ func (w *world) register(c connect) { outbox: make(chan wire.Response, 8), pending: &Request{ From: c.login.Name, - Seq: 1, + Seq: 90, Wants: &spawnPlayer{}, }, } diff --git a/internal/wire/client.go b/internal/wire/client.go index 8cd7b80..bde5ef0 100644 --- a/internal/wire/client.go +++ b/internal/wire/client.go @@ -164,7 +164,7 @@ func (c *Client) writeLoop() { case res := <-c.resolved: p, ok := sent[res.Re] if !ok { - c.Error("saw response for unknown seq %d") + c.Error("saw response for unknown seq %d", res.Re) break } delete(sent, res.Re) diff --git a/main.go b/main.go index 7af620b..e27b615 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ func main() { switch os.Args[1] { case "client": - runClient(os.Args[2]) + runClient() case "server": s := sim.Server{} if err := s.Start("cdm.jordanorelli.com", 12805); err != nil { @@ -51,7 +51,7 @@ func main() { } } -func runClient(name string) { +func runClient() { log := newLog("./astro.log").Child("client") start := time.Now() @@ -63,8 +63,7 @@ func runClient(name string) { }() app := app.UI{ - Log: log.Child("ui"), - PlayerName: name, + Log: log.Child("ui"), } app.Run() }