package game import ( "fmt" "strings" ) // io.c — the message line, the status line, and character input. // maxMsg is io.c MAXMSG: how much message fits before --More--. const maxMsg = NumCols - len("--More--") - 1 // MessageLine is the io.c message machinery: the static msgbuf/newpos // pair plus the related globals (mpos, huh, and the message-behavior // flags). It owns the top line of the screen; attach wires in the // display and input it needs. type MessageLine struct { buf strings.Builder // msgbuf newpos int Mpos int // where cursor is on top line Huh string // the last message printed SaveMsg bool // remember last msg LowerMsg bool // messages should start w/lower case MsgEsc bool // check for ESC from msg's --More-- scr *Screen // the top line lives on scr.Std look func(wakeup bool) // redraw before a --More-- (misc.c look) readChar func() byte // input for --More-- prompts } // Msg displays a message at the top of the screen (io.c msg). It returns // Escape if the player escaped out of a --More--, ^Escape otherwise (the // C convention: callers compare against ESCAPE). func (m *MessageLine) Msg(format string, a ...any) int { // if the string is "", just clear the line if format == "" { m.scr.Std.Move(0, 0) m.scr.Std.Clrtoeol() m.Mpos = 0 return ^Escape } // otherwise add to the message and flush it out m.doaddf(format, a...) return m.End() } // Addf adds things to the current message (io.c addmsg). func (m *MessageLine) Addf(format string, a ...any) { m.doaddf(format, a...) } // End displays a new msg, giving the player a chance to see the previous // one if it is up there with the --More-- (io.c endmsg). func (m *MessageLine) End() int { if m.SaveMsg { m.Huh = m.buf.String() } if m.Mpos != 0 { m.look(false) m.scr.Std.MvAddStr(0, m.Mpos, "--More--") m.scr.Refresh() if !m.MsgEsc { m.waitForSpace() } else { for { ch := m.readChar() if ch == ' ' { break } if ch == Escape { m.buf.Reset() m.Mpos = 0 m.newpos = 0 return Escape } } } } // All messages should start with uppercase, except ones that start // with a pack addressing character out := m.buf.String() if len(out) > 0 && isLower(out[0]) && !m.LowerMsg && (len(out) <= 1 || out[1] != ')') { out = string(toUpper(out[0])) + out[1:] } m.scr.Std.MvAddStr(0, 0, out) m.scr.Std.Clrtoeol() m.Mpos = m.newpos m.newpos = 0 m.buf.Reset() m.scr.Refresh() return ^Escape } // attach wires the message line to its display and input; NewGame and // Restore call it once the screen and game exist. func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) { m.scr = scr m.look = look m.readChar = readChar } // waitForSpace absorbs input until the player types a space: the // --More-- acknowledgement (io.c wait_for). func (m *MessageLine) waitForSpace() { for { if m.readChar() == ' ' { return } } } // doaddf performs an add onto the message buffer (io.c doadd). func (m *MessageLine) doaddf(format string, a ...any) { s := fmt.Sprintf(format, a...) if len(s)+m.newpos >= maxMsg { m.End() } m.buf.WriteString(s) m.newpos = m.buf.Len() } // msg, addmsgf, and endmsg are the game-side shorthands for the message // line; the machinery lives on MessageLine. func (g *RogueGame) msg(format string, a ...any) int { return g.Msgs.Msg(format, a...) } func (g *RogueGame) addmsgf(format string, a ...any) { g.Msgs.Addf(format, a...) } func (g *RogueGame) endmsg() { g.Msgs.End() } // stepOk returns true if it is ok to step on ch (io.c step_ok). func stepOk(ch byte) bool { switch ch { case ' ', '|', '-': return false default: return !isAlpha(ch) } } // readchar reads and returns a character, checking for gross input errors // (io.c readchar). func (g *RogueGame) readchar() byte { ch := g.scr.term.ReadChar() if ch == 3 { // ^C g.quit(0) return 27 } return ch } // statusCache is the set of static shadow variables in io.c status() that // suppress redundant status-line redraws. type statusCache struct { hpwidth int hungry int lvl int pur int hp int arm int str int exp int init bool } // status displays the important stats line, keeping the cursor where it was // (io.c status). func (g *RogueGame) status() { s := &g.statusCache p := &g.Player // If nothing has changed since the last status, don't bother. temp := p.Stats.ArmorClass if p.CurArmor != nil { temp = p.CurArmor.ArmorClass } if s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp && s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str && s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg { return } s.init = true s.arm = temp oy, ox := g.scr.Std.GetYX() if s.hp != p.Stats.MaxHP { s.hp = p.Stats.MaxHP s.hpwidth = 0 for t := p.Stats.MaxHP; t != 0; t /= 10 { s.hpwidth++ } } // Save current status s.lvl = g.Depth s.pur = p.Purse s.hp = p.Stats.HP s.str = p.Stats.Str s.exp = p.Stats.Exp s.hungry = p.HungryState line := fmt.Sprintf( "Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s", g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP, p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp, g.data.hungerStateName[p.HungryState]) if g.StatMsg { g.move(0, 0) g.msg("%s", line) } else { g.move(StatLine, 0) g.addstr(line) } g.clrtoeol() g.move(oy, ox) } // waitFor sits around until the guy types the right key (io.c wait_for). func (g *RogueGame) waitFor(ch byte) { if ch == '\n' { for { c := g.readchar() if c == '\n' || c == '\r' { return } } } for { if g.readchar() == ch { return } } } // showWin displays a window and waits before returning (io.c show_win). func (g *RogueGame) showWin(message string) { g.scr.Hw.MvAddStr(0, 0, message) g.scr.Hw.Move(g.Player.Pos.Y, g.Player.Pos.X) g.scr.RefreshWin(g.scr.Hw) g.waitFor(' ') g.refresh() } // ASCII helpers standing in for ; the C game only ever handles // 7-bit characters. func isAlpha(c byte) bool { return isUpper(c) || isLower(c) } func isUpper(c byte) bool { return c >= 'A' && c <= 'Z' } func isLower(c byte) bool { return c >= 'a' && c <= 'z' } func isDigit(c byte) bool { return c >= '0' && c <= '9' } func isPrint(c byte) bool { return c >= ' ' && c < 0x7f } func toUpper(c byte) byte { if isLower(c) { return c - 'a' + 'A' } return c } func toLower(c byte) byte { if isUpper(c) { return c - 'A' + 'a' } return c }