fishing mechanic in ssh and fixed rotating host key
This commit is contained in:
+191
-59
@@ -6,8 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
@@ -132,7 +134,7 @@ func main() {
|
||||
// The SSH server need its own keys, this will create a keypair in the
|
||||
// given path if it doesn't exist yet.
|
||||
// By default, it will create an ED25519 key.
|
||||
wish.WithHostKeyPath(".ssh/id_ed25519"),
|
||||
wish.WithHostKeyPath("/data/.ssh/ssh_host_ed25519_key"),
|
||||
|
||||
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
angler, err := db.GetAnglerByKey(key)
|
||||
@@ -207,6 +209,15 @@ const (
|
||||
PageGameplay
|
||||
)
|
||||
|
||||
type playerState int
|
||||
|
||||
const (
|
||||
PlayerIdle playerState = iota
|
||||
PlayerCasting
|
||||
PlayerCaught
|
||||
PlayerFailed
|
||||
)
|
||||
|
||||
type tab int
|
||||
|
||||
const (
|
||||
@@ -217,21 +228,25 @@ const (
|
||||
)
|
||||
|
||||
type model struct {
|
||||
profile string
|
||||
term string
|
||||
width int
|
||||
height int
|
||||
bg string
|
||||
DB *db.Database
|
||||
page page
|
||||
angler models.Angler
|
||||
tabs []string
|
||||
tab tab
|
||||
error string
|
||||
ti textinput.Model
|
||||
spinner spinner.Model
|
||||
active tea.Model
|
||||
cast models.Cast
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
profile string
|
||||
term string
|
||||
width int
|
||||
height int
|
||||
bg string
|
||||
DB *db.Database
|
||||
page page
|
||||
angler models.Angler
|
||||
tabs []string
|
||||
tab tab
|
||||
error string
|
||||
ti textinput.Model
|
||||
spinner spinner.Model
|
||||
active tea.Model
|
||||
cast models.Cast
|
||||
playerState playerState
|
||||
message string
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
@@ -251,6 +266,8 @@ func (m model) Init() tea.Cmd {
|
||||
)
|
||||
}
|
||||
|
||||
const chooseNamePrompt = "aye fresh meat, got a name laddie?"
|
||||
|
||||
func (m model) ChooseNameView() tea.View {
|
||||
var error string
|
||||
|
||||
@@ -258,7 +275,9 @@ func (m model) ChooseNameView() tea.View {
|
||||
error = lipgloss.NewStyle().Foreground(lipgloss.Red).Render(m.error)
|
||||
}
|
||||
|
||||
input := lipgloss.JoinVertical(lipgloss.Top, "aye fresh meat, got a name laddie?", m.ti.View(), error)
|
||||
// m.ti.SetWidth(lipgloss.Width(chooseNamePrompt))
|
||||
|
||||
input := lipgloss.JoinVertical(lipgloss.Top, chooseNamePrompt, m.ti.View(), error)
|
||||
|
||||
center := lipgloss.Place(
|
||||
m.width,
|
||||
@@ -284,15 +303,15 @@ func (m model) ChooseNameView() tea.View {
|
||||
bottom,
|
||||
)
|
||||
|
||||
var c *tea.Cursor
|
||||
// var c *tea.Cursor
|
||||
|
||||
c = m.ti.Cursor()
|
||||
// c = m.ti.Cursor()
|
||||
|
||||
c.Y += lipgloss.Height("aye fresh meat, got a name laddie?") + (m.height-3-lipgloss.Height(input))/2
|
||||
c.X += (m.width - lipgloss.Width(input)) / 2
|
||||
// c.Y += lipgloss.Height(prompt) + (m.height-3-lipgloss.Height(input))/2
|
||||
// c.X += (m.width - lipgloss.Width(input)) / 2
|
||||
|
||||
v := tea.NewView(str)
|
||||
v.Cursor = c
|
||||
// v.Cursor = c
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
@@ -343,9 +362,45 @@ func (m model) ChooseNameUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||
var buttonStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Background(lipgloss.Color("8"))
|
||||
var buttonActiveStyle = buttonStyle.Background(lipgloss.Color("30"))
|
||||
|
||||
var keybindKeyStyle = lipgloss.NewStyle()
|
||||
var keybindActionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
|
||||
var sidebarBtnStyle = lipgloss.NewStyle().Padding(0, 2).Align(lipgloss.Center).Foreground(lipgloss.Color("30"))
|
||||
var sidebarBtnActiveStyle = sidebarBtnStyle.UnsetForeground().Background(lipgloss.Color("30"))
|
||||
|
||||
func (m model) GameplayFishingView(width int, height int) string {
|
||||
help := keybindKeyStyle.Render("c") + keybindActionStyle.Render(" cast") + "\t" + keybindKeyStyle.Render("x") + keybindActionStyle.Render(" cancel")
|
||||
|
||||
switch m.playerState {
|
||||
case PlayerIdle:
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().MarginBottom(1).Render(fmt.Sprintf("Aye matie %s...time to get fishin'", m.angler.Username)),
|
||||
help,
|
||||
)
|
||||
case PlayerCasting:
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().MarginBottom(1).Render(m.spinner.View()+" waitin' for a bite"),
|
||||
help,
|
||||
)
|
||||
case PlayerCaught:
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().MarginBottom(1).Render(m.message),
|
||||
help,
|
||||
)
|
||||
case PlayerFailed:
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().MarginBottom(1).Foreground(lipgloss.Red).Render(m.message),
|
||||
help,
|
||||
)
|
||||
}
|
||||
|
||||
return "bad player state"
|
||||
}
|
||||
|
||||
func (m model) GameplayView() tea.View {
|
||||
|
||||
var renderedTabs []string
|
||||
@@ -365,28 +420,33 @@ func (m model) GameplayView() tea.View {
|
||||
|
||||
paddingY, paddingX := 1, 2
|
||||
|
||||
content := lipgloss.NewStyle().
|
||||
Width(m.width-paddingX*2).
|
||||
Height(m.height-2-paddingY*2).
|
||||
maxWidth := min(m.width, 80)
|
||||
maxHeight := min(m.height, 80)
|
||||
|
||||
var content string
|
||||
|
||||
if m.tab == TabFishin {
|
||||
content = m.GameplayFishingView(maxWidth, maxHeight)
|
||||
}
|
||||
|
||||
wrapped := lipgloss.NewStyle().
|
||||
Width(maxWidth-paddingX*2).
|
||||
Height(maxHeight-2-paddingY*2).
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("30")).
|
||||
Padding(paddingY, paddingX).
|
||||
Render(lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().MarginBottom(1).Render(fmt.Sprintf("Aye matie %s...time to get fishin'", m.angler.Username)),
|
||||
buttonActiveStyle.Render("Cast"),
|
||||
))
|
||||
Render(content)
|
||||
|
||||
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render("(esc to quit)")
|
||||
|
||||
box := lipgloss.NewStyle().
|
||||
Width(m.width).
|
||||
Height(m.height).
|
||||
Width(maxWidth).
|
||||
Height(maxHeight).
|
||||
Padding(paddingY, paddingX).
|
||||
Render(lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
tabs,
|
||||
content,
|
||||
wrapped,
|
||||
footer,
|
||||
))
|
||||
|
||||
@@ -414,6 +474,8 @@ func (m model) GameplayView() tea.View {
|
||||
type GameplayKeyMap struct {
|
||||
CycleTab key.Binding
|
||||
AltCycleTab key.Binding
|
||||
Cast key.Binding
|
||||
Cancel key.Binding
|
||||
Interact key.Binding
|
||||
}
|
||||
|
||||
@@ -426,6 +488,14 @@ var DefaultGameplayKeyMap = GameplayKeyMap{
|
||||
key.WithKeys("shift+tab"),
|
||||
key.WithHelp("shift+tab", "Cycle Tab Left"),
|
||||
),
|
||||
Cast: key.NewBinding(
|
||||
key.WithKeys("c"),
|
||||
key.WithHelp("c", "Cast"),
|
||||
),
|
||||
Cancel: key.NewBinding(
|
||||
key.WithKeys("x"),
|
||||
key.WithHelp("x", "Cancel action."),
|
||||
),
|
||||
Interact: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "Interact with selected option"),
|
||||
@@ -464,6 +534,35 @@ var sayings = []string{
|
||||
"Catching the wind in a net",
|
||||
}
|
||||
|
||||
type castFinishedMsg struct {
|
||||
caught bool
|
||||
err error
|
||||
}
|
||||
|
||||
func waitForCast(
|
||||
ctx context.Context,
|
||||
wait time.Duration,
|
||||
) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
timer := time.NewTimer(wait)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
caught := rand.IntN(100) < 75
|
||||
|
||||
return castFinishedMsg{
|
||||
caught: caught,
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return castFinishedMsg{
|
||||
err: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
@@ -486,26 +585,53 @@ func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||
} else {
|
||||
m.tab -= 1
|
||||
}
|
||||
case key.Matches(msg, DefaultGameplayKeyMap.Interact):
|
||||
case key.Matches(msg, DefaultGameplayKeyMap.Cast):
|
||||
m.playerState = PlayerCasting
|
||||
|
||||
// if m.active = {
|
||||
is_casting, err := m.DB.IsCasting(m.angler.ID)
|
||||
|
||||
if is_casting {
|
||||
m.playerState = PlayerIdle
|
||||
m.error = "looks like you are already casting somewhere"
|
||||
// TODO: clear after a timeout
|
||||
} else {
|
||||
m.cast, err = m.DB.CreateNewCast(m.angler.ID)
|
||||
|
||||
if err != nil {
|
||||
m.playerState = PlayerIdle
|
||||
m.error = "unable to start your cast"
|
||||
log.Print(err)
|
||||
} else {
|
||||
// Fishing can take anywhere from 2-5 seconds
|
||||
minTime, maxTime := 2, 5
|
||||
timeToWait := time.Duration(rand.IntN(maxTime-minTime+1)+minTime) * time.Second
|
||||
|
||||
cmds = append(cmds, waitForCast(m.ctx, timeToWait), m.spinner.Tick)
|
||||
}
|
||||
}
|
||||
|
||||
m.cast, err = m.DB.CreateNewCast(m.angler.ID)
|
||||
|
||||
if err != nil {
|
||||
m.error = "unable to start your cast"
|
||||
// TODO: handle error
|
||||
}
|
||||
|
||||
// TODO: implement rest of casting logic
|
||||
|
||||
// }
|
||||
}
|
||||
case castFinishedMsg:
|
||||
_, err := m.DB.EndCastByID(m.cast.ID)
|
||||
|
||||
if err != nil {
|
||||
m.error = "unable to end your cast"
|
||||
}
|
||||
|
||||
if msg.err != nil {
|
||||
m.error = msg.err.Error()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if msg.caught {
|
||||
m.playerState = PlayerCaught
|
||||
m.message = "Got one! Nice!"
|
||||
} else {
|
||||
m.playerState = PlayerFailed
|
||||
m.message = sayings[rand.IntN(len(sayings))]
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
@@ -515,7 +641,7 @@ func (m model) GameplayUpdate(msg tea.Msg) (model, tea.Cmd) {
|
||||
|
||||
func (m model) gotoGameplayView() (model, tea.Cmd) {
|
||||
m.page = PageGameplay
|
||||
return m, m.spinner.Tick
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type GlobalKeyMap struct {
|
||||
@@ -579,19 +705,25 @@ func teaHandler(DB *db.Database) func(ssh.Session) (tea.Model, []tea.ProgramOpti
|
||||
return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
pty, _, _ := s.Pty()
|
||||
angler, _ := s.Context().Value("angler").(models.Angler)
|
||||
ctx, cancel := context.WithCancel(s.Context())
|
||||
|
||||
m := model{
|
||||
term: pty.Term,
|
||||
width: pty.Window.Width,
|
||||
height: pty.Window.Height,
|
||||
bg: "light",
|
||||
DB: DB,
|
||||
page: PageChooseName,
|
||||
angler: angler,
|
||||
tabs: []string{"fishin'", "wares", "dock", "settings"},
|
||||
tab: TabFishin,
|
||||
ti: textinput.New(),
|
||||
spinner: spinner.New(),
|
||||
// using ssh context for now
|
||||
// TODO: may make sens to use a custom one
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
term: pty.Term,
|
||||
width: pty.Window.Width,
|
||||
height: pty.Window.Height,
|
||||
bg: "light",
|
||||
DB: DB,
|
||||
page: PageChooseName,
|
||||
angler: angler,
|
||||
tabs: []string{"fishin'", "wares", "dock", "settings"},
|
||||
tab: TabFishin,
|
||||
ti: textinput.New(),
|
||||
spinner: spinner.New(),
|
||||
playerState: PlayerIdle,
|
||||
}
|
||||
|
||||
if angler.Username == gossh.FingerprintSHA256(s.PublicKey()) {
|
||||
@@ -603,10 +735,10 @@ func teaHandler(DB *db.Database) func(ssh.Session) (tea.Model, []tea.ProgramOpti
|
||||
m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("30"))
|
||||
m.spinner.Spinner = spinner.Jump
|
||||
|
||||
m.ti.SetVirtualCursor(false)
|
||||
// m.ti.SetVirtualCursor(false)
|
||||
m.ti.SetWidth(lipgloss.Width(chooseNamePrompt) - 3)
|
||||
m.ti.Focus()
|
||||
m.ti.CharLimit = 156
|
||||
m.ti.SetWidth(20)
|
||||
|
||||
return m, []tea.ProgramOption{}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ services:
|
||||
COOKIE_STORE_SECRET: ${COOKIE_STORE_SECRET}
|
||||
DB_CONNECTION_STRING: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
ENV: ${ENV}
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "22:22"
|
||||
- "9900:9900"
|
||||
|
||||
Reference in New Issue
Block a user