From 4c8aac46cbd18091eb54a2042514d28792ac0b0a Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 4 Jul 2022 17:45:15 -0400 Subject: [PATCH] bubble up window size properly, add journal pages --- data/journal.toml | 13 +++++ jmenu.go | 84 +++++++++++++++++++++++++++ jview.go | 128 +++++++++++++++++++++++++++++++++++++++++ keymap.go | 7 ++- main.go | 2 +- mainscreen.go | 34 ++++++++--- menu.go | 15 +++-- pagelist.go | 36 ++++++++++++ parent.go | 2 +- simulator/journal.go | 38 ++++++++++++ simulator/player.go | 29 +++++++++- simulator/simulator.go | 6 ++ 12 files changed, 379 insertions(+), 15 deletions(-) create mode 100644 data/journal.toml create mode 100644 jmenu.go create mode 100644 jview.go create mode 100644 pagelist.go create mode 100644 simulator/journal.go diff --git a/data/journal.toml b/data/journal.toml new file mode 100644 index 0000000..44238aa --- /dev/null +++ b/data/journal.toml @@ -0,0 +1,13 @@ +[[journalpage]] +pageid = 0 +title = "It Begins" +content = "So much to learn" +[[journalpage.requires]] + +[[journalpage]] +pageid = 1 +title = "Building" +content = "Craft that motherfucking tea button" +[[journalpage.requires]] +name = "tea" +value = 10 diff --git a/jmenu.go b/jmenu.go new file mode 100644 index 0000000..c0e5c14 --- /dev/null +++ b/jmenu.go @@ -0,0 +1,84 @@ +package main + +import ( + sim "git.saintnet.tech/stryan/spacetea/simulator" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type jEntry struct { + title, id string +} + +func (i jEntry) Title() string { return i.title } +func (i jEntry) Description() string { return "" } +func (i jEntry) ID() string { return i.id } +func (i jEntry) FilterValue() string { return i.title } + +type jMenuModel struct { + list list.Model + lastSize tea.WindowSizeMsg +} + +type readMsg string + +func newJMenuModel(pages []sim.JournalPage) jMenuModel { + var jm jMenuModel + items := []list.Item{} + for _, v := range pages { + items = append(items, jEntry{v.Title, v.ID()}) + } + jm.list = list.New(items, list.NewDefaultDelegate(), 80, 32) + jm.list.DisableQuitKeybindings() + jm.list.Title = "Read which entry?" + return jm +} + +func (j jMenuModel) Init() tea.Cmd { + return tea.EnterAltScreen +} + +// Update is called when a message is received. Use it to inspect messages +// and, in response, update the model and/or send a command. +func (j jMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + h, v := docStyle.GetFrameSize() + j.list.SetSize(msg.Width-h, msg.Height-v) + j.lastSize = msg + return j, nil + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "ctrl-c": + return j, tea.Quit + case "esc": + return initMainscreen(), tea.Batch(j.GetSize, heartbeat()) + case "enter": + return initMainscreen(), tea.Batch(j.GetSize, j.buildJmenuMsg, heartbeat()) + } + } + + var cmd tea.Cmd + j.list, cmd = j.list.Update(msg) + return j, cmd + +} + +// View renders the program's UI, which is just a string. The view is +// rendered after every Update. +func (j jMenuModel) View() string { + return j.list.View() +} + +func (j jMenuModel) buildJmenuMsg() tea.Msg { + if j.list.SelectedItem() == nil { + return "" + } + i := j.list.SelectedItem().(jEntry) + return readMsg(i.ID()) + +} + +func (m jMenuModel) GetSize() tea.Msg { + return m.lastSize +} diff --git a/jview.go b/jview.go new file mode 100644 index 0000000..d981d73 --- /dev/null +++ b/jview.go @@ -0,0 +1,128 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const useHighPerformanceRenderer = false + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + infoStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return titleStyle.Copy().BorderStyle(b) + }() +) + +type jview struct { + title string + content string + ready bool + lastSize tea.WindowSizeMsg + viewport viewport.Model +} + +func newJView(title, content string) jview { + return jview{title: title, content: content} +} + +func (m jview) Init() tea.Cmd { + return nil +} + +func (m jview) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmd tea.Cmd + cmds []tea.Cmd + ) + + switch msg := msg.(type) { + case tea.KeyMsg: + if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { + return initMainscreen(), tea.Batch(m.GetSize, heartbeat()) + } + + case tea.WindowSizeMsg: + m.lastSize = msg + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + verticalMarginHeight := headerHeight + footerHeight + + if !m.ready { + // Since this program is using the full size of the viewport we + // need to wait until we've received the window dimensions before + // we can initialize the viewport. The initial dimensions come in + // quickly, though asynchronously, which is why we wait for them + // here. + m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + m.viewport.YPosition = headerHeight + m.viewport.HighPerformanceRendering = useHighPerformanceRenderer + m.viewport.SetContent(m.content) + m.ready = true + + // This is only necessary for high performance rendering, which in + // most cases you won't need. + // + // Render the viewport one line below the header. + m.viewport.YPosition = headerHeight + 1 + } else { + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height - verticalMarginHeight + } + + if useHighPerformanceRenderer { + // Render (or re-render) the whole viewport. Necessary both to + // initialize the viewport and when the window is resized. + // + // This is needed for high-performance rendering only. + cmds = append(cmds, viewport.Sync(m.viewport)) + } + } + + // Handle keyboard and mouse events in the viewport + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m jview) View() string { + if !m.ready { + return "\n Initializing..." + } + return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) +} + +func (m jview) headerView() string { + title := titleStyle.Render(m.title) + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title))) + return lipgloss.JoinHorizontal(lipgloss.Center, title, line) +} + +func (m jview) footerView() string { + info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func (m jview) GetSize() tea.Msg { + return m.lastSize +} diff --git a/keymap.go b/keymap.go index 933783b..18f9d73 100644 --- a/keymap.go +++ b/keymap.go @@ -14,6 +14,7 @@ type keyMap struct { Destroy key.Binding Place key.Binding Craft key.Binding + Journal key.Binding } // ShortHelp returns keybindings to be shown in the mini help view. It's part @@ -26,7 +27,7 @@ func (k keyMap) ShortHelp() []key.Binding { // key.Map interface. func (k keyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ - {k.Up, k.Down, k.Left, k.Right, k.Gather, k.Place, k.Craft, k.Destroy, k.Pickup, k.Help, k.Quit}, //second column + {k.Up, k.Down, k.Left, k.Right, k.Gather, k.Place, k.Craft, k.Destroy, k.Pickup, k.Journal, k.Help, k.Quit}, //second column } } @@ -75,4 +76,8 @@ var keys = keyMap{ key.WithKeys("c"), key.WithHelp("c", "craft object"), ), + Journal: key.NewBinding( + key.WithKeys("v"), + key.WithHelp("v", "view journal"), + ), } diff --git a/main.go b/main.go index 1c3f822..2a7fd78 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ func main() { simulator = sim.NewSimulator() simulator.Start() parent := parent{initMainscreen()} - if err := tea.NewProgram(parent).Start(); err != nil { + if err := tea.NewProgram(parent, tea.WithAltScreen()).Start(); err != nil { fmt.Printf("Uh oh, there was an error: %v\n", err) os.Exit(1) } diff --git a/mainscreen.go b/mainscreen.go index 2bf907a..557d452 100644 --- a/mainscreen.go +++ b/mainscreen.go @@ -22,11 +22,12 @@ var style = lipgloss.NewStyle(). BorderForeground(lipgloss.Color("63")) type model struct { - s *sim.Simulator - input textinput.Model - help help.Model - keys keyMap - next string + s *sim.Simulator + input textinput.Model + help help.Model + keys keyMap + next string + lastSize tea.WindowSizeMsg } type beat struct{} @@ -71,6 +72,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, heartbeat() case tea.WindowSizeMsg: m.help.Width = msg.Width + m.lastSize = msg + case placeMsg: simc := fmt.Sprintf("place %v", string(msg)) m.s.Input(simc) @@ -79,6 +82,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { simc := fmt.Sprintf("craft %v", string(msg)) m.s.Input(simc) return m, nil + case readMsg: + pid, err := strconv.Atoi(string(msg)) + if err != nil { + panic(err) + } + return newJView(sim.GlobalPages[pid].Title, sim.GlobalPages[pid].Content), m.GetSize case tea.KeyMsg: switch { case key.Matches(msg, m.keys.Help): @@ -109,7 +118,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } sort.Sort(res) - return newMenuModel(res, placeMenu), nil + return newMenuModel(res, placeMenu), m.GetSize case key.Matches(msg, m.keys.Pickup): m.s.Input("pickup") case key.Matches(msg, m.keys.Destroy): @@ -120,7 +129,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { res = append(res, sim.GlobalItems[k].(sim.ItemEntry)) } sort.Sort(res) - return newMenuModel(res, craftMenu), nil + return newMenuModel(res, craftMenu), m.GetSize + case key.Matches(msg, m.keys.Journal): + var res pagelist + for k := range m.s.Player.Pages { + res = append(res, sim.GlobalPages[k]) + } + sort.Sort(res) + return newJMenuModel(res), m.GetSize } } m.input, cmd = m.input.Update(msg) @@ -140,3 +156,7 @@ func (m model) View() string { } return render } + +func (m model) GetSize() tea.Msg { + return m.lastSize +} diff --git a/menu.go b/menu.go index fcef07e..4da7031 100644 --- a/menu.go +++ b/menu.go @@ -14,6 +14,7 @@ type menutype int const ( placeMenu menutype = iota craftMenu + journalMenu ) type item struct { @@ -27,8 +28,9 @@ func (i item) ID() string { return i.id } func (i item) FilterValue() string { return i.title } type menuModel struct { - list list.Model - kind menutype + list list.Model + kind menutype + lastSize tea.WindowSizeMsg } type placeMsg string @@ -80,15 +82,16 @@ func (p menuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: h, v := docStyle.GetFrameSize() p.list.SetSize(msg.Width-h, msg.Height-v) + p.lastSize = msg return p, nil case tea.KeyMsg: switch keypress := msg.String(); keypress { case "ctrl-c": return p, tea.Quit case "esc": - return initMainscreen(), heartbeat() + return initMainscreen(), tea.Batch(p.GetSize, heartbeat()) case "enter": - return initMainscreen(), tea.Batch(p.buildMenuMsg, heartbeat()) + return initMainscreen(), tea.Batch(p.GetSize, p.buildMenuMsg, heartbeat()) } } @@ -102,3 +105,7 @@ func (p menuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (p menuModel) View() string { return p.list.View() } + +func (p menuModel) GetSize() tea.Msg { + return p.lastSize +} diff --git a/pagelist.go b/pagelist.go new file mode 100644 index 0000000..d7a5a5a --- /dev/null +++ b/pagelist.go @@ -0,0 +1,36 @@ +package main + +import sim "git.saintnet.tech/stryan/spacetea/simulator" + +type pagelist []sim.JournalPage + +// Len is the number of elements in the collection. +func (e pagelist) Len() int { + return len(e) +} + +// Less reports whether the element with index i +// must sort before the element with index j. +// +// If both Less(i, j) and Less(j, i) are false, +// then the elements at index i and j are considered equal. +// Sort may place equal elements in any order in the final result, +// while Stable preserves the original input order of equal elements. +// +// Less must describe a transitive ordering: +// - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well. +// - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well. +// +// Note that floating-point comparison (the < operator on float32 or float64 values) +// is not a transitive ordering when not-a-number (NaN) values are involved. +// See Float64Slice.Less for a correct implementation for floating-point values. +func (e pagelist) Less(i int, j int) bool { + return e[i].ID() < e[j].ID() +} + +// Swap swaps the elements with indexes i and j. +func (e pagelist) Swap(i int, j int) { + tmp := e[i] + e[i] = e[j] + e[j] = tmp +} diff --git a/parent.go b/parent.go index 4b462c6..c03b23b 100644 --- a/parent.go +++ b/parent.go @@ -7,7 +7,7 @@ type parent struct { } func (m parent) Init() tea.Cmd { - return tea.Batch(m.current.Init(), tea.EnterAltScreen) + return tea.Batch(tea.EnterAltScreen, m.current.Init()) } func (m parent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/simulator/journal.go b/simulator/journal.go new file mode 100644 index 0000000..191a4e8 --- /dev/null +++ b/simulator/journal.go @@ -0,0 +1,38 @@ +package simulator + +import ( + "strconv" + + "github.com/BurntSushi/toml" +) + +//PageID is a journal page +type PageID int + +//JournalPage is a "flavour event" +type JournalPage struct { + PageID PageID `toml:"pageid"` + Title string `toml:"title"` + Content string `toml:"content"` + Requires []relation `toml:"requires"` +} + +func (j JournalPage) ID() string { + return strconv.Itoa(int(j.PageID)) +} + +//GlobalPages is a list of all pages +var GlobalPages []JournalPage + +type pages struct { + JournalPage []JournalPage +} + +func loadPages(filename string) { + var res pages + _, err := toml.DecodeFile(filename, &res) + if err != nil { + panic(err) + } + GlobalPages = res.JournalPage +} diff --git a/simulator/player.go b/simulator/player.go index 7b27106..71ed9e4 100644 --- a/simulator/player.go +++ b/simulator/player.go @@ -11,6 +11,7 @@ type Player struct { Resources map[itemType]int Craftables map[itemType]struct{} Techs map[TechID]Tech + Pages map[PageID]JournalPage CurrentTile *Tile log []string logIndex int @@ -18,7 +19,13 @@ type Player struct { //NewPlayer initializes a player func NewPlayer() *Player { - return &Player{Resources: make(map[itemType]int), Techs: make(map[TechID]Tech), Craftables: make(map[itemType]struct{})} + p := &Player{ + Resources: make(map[itemType]int), + Techs: make(map[TechID]Tech), + Craftables: make(map[itemType]struct{}), + Pages: make(map[PageID]JournalPage), + } + return p } //AddItemByName adds the given amount of the item using the item name @@ -100,6 +107,26 @@ func (p *Player) research() { } } +func (p *Player) journal() { + for _, page := range GlobalPages { + if _, ok := p.Pages[page.PageID]; ok { + continue + } + + i := 0 + for _, v := range page.Requires { + req := lookupByName(v.Name) + if p.Resources[req.ID()] >= v.Value { + i++ + } + } + if i == len(page.Requires) { + p.Pages[page.PageID] = page + p.Announce(fmt.Sprintf("New Journal: %v", page.Title)) + } + } +} + //Announce adds an entry to a players log func (p *Player) Announce(msg string) { p.logIndex++ diff --git a/simulator/simulator.go b/simulator/simulator.go index 88dfa93..56adfcb 100644 --- a/simulator/simulator.go +++ b/simulator/simulator.go @@ -28,12 +28,17 @@ func NewSimulator() *Simulator { loadResources("data/resources.toml") log.Println("loading converters") loadConverters("data/converters.toml") + log.Println("loading journal") + loadPages("data/journal.toml") if len(GlobalItems) < 1 { panic("Loaded items but nothing in global items table") } if len(GlobalTechs) < 1 { panic("Loaded items but nothing in global items table") } + if len(GlobalPages) < 1 { + panic("Loaded journal but no pages in table") + } pod.Place(newResource(lookupByName("tea").ID()), 4, 4) player.AddItem(itemType(1), 30) player.AddItem(itemType(3), 5) @@ -167,6 +172,7 @@ func (s *Simulator) main() { s.Time = s.Time + 1 s.Place.Tick() s.Player.research() + s.Player.journal() } } }