Files
rgoue/game/command.go
sneak e71a2ef3fd Rename constants to descriptive names (refactor step 1)
Pure rename, no behavior change; the full suite (RNG goldens,
generation invariants, scripted sessions, save round trip) is
unchanged and green.

- Creature flags: IsHuh→Confused, IsHalu→Hallucinating,
  CanHuh→CanConfuse, CanSee→CanSeeInvisible, IsRun→Awake,
  SeeMonst→SenseMonsters, IsCanc→Cancelled, IsLevit→Levitating,
  IsBlind/IsGreed/IsHaste/IsTarget/IsHeld/IsInvis/IsMean/IsRegen/
  IsFly/IsSlow → Blind/Greedy/Hasted/Targeted/Held/Invisible/Mean/
  Regenerates/Flying/Slowed, IsFound→Found
- Object flags: IsCursed→Cursed, IsKnow→Known, IsMissl→Missile,
  IsMany→Stackable, ObjIsFound→WasFound, IsProt→Protected
- Room flags: IsDark/IsGone/IsMaze → Dark/Gone/Maze; place flags:
  FPass→FPassage, FPNum→FPassNum, FTMask→FTrapMask
- Trap types: TDoor→TrapDoor ... TMyst→TrapMystery
- Item subtypes: P*→Potion* (PLSD→PotionLSD, PMFind→
  PotionDetectMonsters, ...), S*→Scroll* (SIDRorS→
  ScrollIdentifyRingOrStick, ...), R*→Ring* (RAddHit→RingDexterity,
  RNop→RingAdornment, ...), Ws*→Wand* (WsElect→WandLightning, ...),
  weapons (TwoSword→WeaponTwoHandedSword, Shiraken→WeaponShuriken,
  ...), armor (RingMail→ArmorRingMail, ...)
- Counts: Max{Potions,Scrolls,Rings,Sticks,Weapons,Armors} →
  Num{Potion,Scroll,Ring,Wand,Weapon,Armor}Types; NTraps→NumTrapTypes
- Level.NTraps field → TrapCount (also in the save snapshot)
- Original C constant names preserved as comment breadcrumbs in
  types.go so the lineage stays greppable

TODO.md: step 1 moved to Completed, step 2 (typed kinds) promoted to
Next Step.
2026-07-06 21:28:57 +02:00

772 lines
15 KiB
Go

package game
// command.c — read and execute the user commands.
// command processes one player command, with its BEFORE/AFTER daemon
// bracketing (command.c command).
func (g *RogueGame) command() {
p := &g.Player
ntimes := 1 // number of player moves
if p.On(Hasted) {
ntimes++
}
// Let the daemons start up
g.DoDaemons(Before)
g.DoFuses(Before)
for ; ntimes > 0; ntimes-- {
g.Again = false
if g.HasHit {
g.endmsg()
g.HasHit = false
}
// these are illegal things for the player to be, so if any are
// set, someone's been poking in memory
if p.On(Slowed | Greedy | Invisible | Regenerates | Targeted) {
panic("player flags corrupted")
}
g.look(true)
if !g.Running {
g.DoorStop = false
}
g.status()
g.LastScore = p.Purse
g.move(p.Pos.Y, p.Pos.X)
if !((g.Running || g.Count != 0) && g.Options.Jump) {
g.refresh() // draw screen
}
g.Take = 0
g.After = true
// Read command or continue run
if g.Wizard {
g.NoScore = true
}
var ch byte
if g.NoCommand == 0 {
if g.Running || g.ToDeath {
ch = g.RunCh
} else if g.Count != 0 {
ch = g.countCh
} else {
ch = g.readchar()
g.MoveOn = false
if g.Msgs.Mpos != 0 { // erase message if its there
g.msg("")
}
}
} else {
ch = '.'
}
if g.NoCommand != 0 {
if g.NoCommand--; g.NoCommand == 0 {
p.Flags.Set(Awake)
g.msg("you can move again")
}
} else {
// check for prefixes
g.newCount = false
if isDigit(ch) {
g.Count = 0
g.newCount = true
for isDigit(ch) {
g.Count = g.Count*10 + int(ch-'0')
if g.Count > 255 {
g.Count = 255
}
ch = g.readchar()
}
g.countCh = ch
// turn off count for commands which don't make sense
// to repeat
switch ch {
case CTRL('B'), CTRL('H'), CTRL('J'), CTRL('K'),
CTRL('L'), CTRL('N'), CTRL('U'), CTRL('Y'),
'.', 'a', 'b', 'h', 'j', 'k', 'l', 'm', 'n', 'q',
'r', 's', 't', 'u', 'y', 'z', 'B', 'C', 'H', 'I',
'J', 'K', 'L', 'N', 'U', 'Y',
CTRL('D'), CTRL('A'):
default:
g.Count = 0
}
}
// execute a command
if g.Count != 0 && !g.Running {
g.Count--
}
if ch != 'a' && ch != Escape &&
!(g.Running || g.Count != 0 || g.ToDeath) {
g.LLastComm = g.LastComm
g.LLastDir = g.LastDir
g.LLastPick = g.LastPick
g.LastComm = ch
g.LastDir = 0
g.LastPick = nil
}
g.dispatch(ch)
// turn off flags if no longer needed
if !g.Running {
g.DoorStop = false
}
}
// If he ran into something to take, let him pick it up.
if g.Take != 0 {
g.pickUp(g.Take)
}
if !g.Running {
g.DoorStop = false
}
if !g.After {
ntimes++
}
}
g.DoDaemons(After)
g.DoFuses(After)
if p.IsRing(Left, RingSearching) {
g.search()
} else if p.IsRing(Left, RingTeleportation) && g.rnd(50) == 0 {
g.teleport()
}
if p.IsRing(Right, RingSearching) {
g.search()
} else if p.IsRing(Right, RingTeleportation) && g.rnd(50) == 0 {
g.teleport()
}
}
// dispatch runs one command character (the big switch in command.c,
// including its `goto over` re-dispatch).
func (g *RogueGame) dispatch(ch byte) {
p := &g.Player
over:
switch ch {
case ',':
var found *Object
for _, obj := range g.Level.Objects {
if obj.Pos.Y == p.Pos.Y && obj.Pos.X == p.Pos.X {
found = obj
break
}
}
if found != nil {
if !g.levitCheck() {
g.pickUp(found.Type)
}
} else {
if !g.Options.Terse {
g.addmsg("there is ")
}
g.addmsg("nothing here")
if !g.Options.Terse {
g.addmsg(" to pick up")
}
g.endmsg()
}
case '!':
g.shell()
case 'h':
g.doMove(0, -1)
case 'j':
g.doMove(1, 0)
case 'k':
g.doMove(-1, 0)
case 'l':
g.doMove(0, 1)
case 'y':
g.doMove(-1, -1)
case 'u':
g.doMove(-1, 1)
case 'b':
g.doMove(1, -1)
case 'n':
g.doMove(1, 1)
case 'H':
g.doRun('h')
case 'J':
g.doRun('j')
case 'K':
g.doRun('k')
case 'L':
g.doRun('l')
case 'Y':
g.doRun('y')
case 'U':
g.doRun('u')
case 'B':
g.doRun('b')
case 'N':
g.doRun('n')
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
if !p.On(Blind) {
g.DoorStop = true
g.Firstmove = true
}
if g.Count != 0 && !g.newCount {
ch = g.direction
} else {
ch += 'A' - CTRL('A')
g.direction = ch
}
goto over
case 'F', 'f':
if ch == 'F' {
g.Kamikaze = true
}
if !g.getDir() {
g.After = false
break
}
g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X
mp := g.Level.MonsterAt(g.Delta.Y, g.Delta.X)
if mp == nil || (!g.seeMonst(mp) && !p.On(SenseMonsters)) {
if !g.Options.Terse {
g.addmsg("I see ")
}
g.msg("no monster there")
g.After = false
} else if g.diagOk(p.Pos, g.Delta) {
g.ToDeath = true
g.MaxHit = 0
mp.Flags.Set(Targeted)
g.RunCh = g.DirCh
ch = g.DirCh
goto over
}
case 't':
if !g.getDir() {
g.After = false
} else {
g.missile(g.Delta.Y, g.Delta.X)
}
case 'a':
if g.LastComm == 0 {
g.msg("you haven't typed a command yet")
g.After = false
} else {
ch = g.LastComm
g.Again = true
goto over
}
case 'q':
g.quaff()
case 'Q':
g.After = false
g.QComm = true
g.quit(0)
g.QComm = false
case 'i':
g.After = false
g.inventory(p.Pack, 0)
case 'I':
g.After = false
g.pickyInven()
case 'd':
g.dropIt()
case 'r':
g.readScroll()
case 'e':
g.eat()
case 'w':
g.wield()
case 'W':
g.wear()
case 'T':
g.takeOff()
case 'P':
g.ringOn()
case 'R':
g.ringOff()
case 'o':
g.option()
g.After = false
case 'c':
g.call()
g.After = false
case '>':
g.After = false
g.dLevel()
case '<':
g.After = false
g.uLevel()
case '?':
g.After = false
g.help()
case '/':
g.After = false
g.identify()
case 's':
g.search()
case 'z':
if g.getDir() {
g.doZap()
} else {
g.After = false
}
case 'D':
g.After = false
g.discovered()
case CTRL('P'):
g.After = false
g.msg("%s", g.Msgs.Huh)
case CTRL('R'):
g.After = false
g.refresh()
case 'v':
g.After = false
g.msg("version %s. (mctesq was here)", Release)
case 'S':
g.After = false
g.saveGame()
case '.':
// rest command
case ' ':
g.After = false // "legal" illegal command
case '^':
g.After = false
if g.getDir() {
g.Delta.Y += p.Pos.Y
g.Delta.X += p.Pos.X
fp := g.Level.FlagsAt(g.Delta.Y, g.Delta.X)
if !g.Options.Terse {
g.addmsg("You have found ")
}
if g.Level.Char(g.Delta.Y, g.Delta.X) != Trap {
g.msg("no trap there")
} else if p.On(Hallucinating) {
g.msg("%s", trName[g.rnd(NumTrapTypes)])
} else {
g.msg("%s", trName[*fp&FTrapMask])
fp.Set(FSeen)
}
}
case Escape: // escape
g.DoorStop = false
g.Count = 0
g.After = false
g.Again = false
case 'm':
g.MoveOn = true
if !g.getDir() {
g.After = false
} else {
ch = g.DirCh
g.countCh = g.DirCh
goto over
}
case ')':
g.current(p.CurWeapon, "wielding", "")
case ']':
g.current(p.CurArmor, "wearing", "")
case '=':
g.current(p.CurRing[Left], "wearing",
g.chooseTerse("(L)", "on left hand"))
g.current(p.CurRing[Right], "wearing",
g.chooseTerse("(R)", "on right hand"))
case '@':
g.StatMsg = true
g.status()
g.StatMsg = false
g.After = false
default:
g.After = false
if g.Wizard {
g.wizardCommand(ch)
} else {
g.illcom(ch)
}
}
}
// wizardCommand handles the MASTER debug commands (command.c).
func (g *RogueGame) wizardCommand(ch byte) {
p := &g.Player
switch ch {
case '|':
g.msg("@ %d,%d", p.Pos.Y, p.Pos.X)
case 'C':
g.createObj()
case '$':
g.msg("inpack = %d", p.Inpack)
case CTRL('G'):
g.inventory(g.Level.Objects, 0)
case CTRL('W'):
g.whatis(false, 0)
case CTRL('D'):
g.Depth++
g.NewLevel()
case CTRL('A'):
g.Depth--
g.NewLevel()
case CTRL('F'):
g.showMap()
case CTRL('T'):
g.teleport()
case CTRL('E'):
g.msg("food left: %d", p.FoodLeft)
case CTRL('C'):
g.addPass()
case CTRL('X'):
g.turnSee(p.On(SenseMonsters))
case CTRL('~'):
if item := g.getItem("charge", int(Stick)); item != nil {
item.SetCharges(10000)
}
case CTRL('I'):
for range 9 {
g.raiseLevel()
}
// Give him a sword (+1,+1)
obj := newObject()
g.initWeapon(obj, WeaponTwoHandedSword)
obj.HPlus = 1
obj.DPlus = 1
g.addPack(obj, true)
p.CurWeapon = obj
// And his suit of armor
obj = newObject()
obj.Type = Armor
obj.Which = ArmorPlateMail
obj.Arm = -5
obj.Flags.Set(Known)
obj.Count = 1
p.CurArmor = obj
g.addPack(obj, true)
case '*':
g.prList()
default:
g.illcom(ch)
}
}
// illcom reports an illegal command (command.c illcom).
func (g *RogueGame) illcom(ch byte) {
g.Msgs.SaveMsg = false
g.Count = 0
g.msg("illegal command '%s'", unctrl(ch))
g.Msgs.SaveMsg = true
}
// search gropes about to find hidden things (command.c search).
func (g *RogueGame) search() {
p := &g.Player
ey := p.Pos.Y + 1
ex := p.Pos.X + 1
probinc := 0
if p.On(Hallucinating) {
probinc = 3
}
if p.On(Blind) {
probinc += 2
}
found := false
for y := p.Pos.Y - 1; y <= ey; y++ {
for x := p.Pos.X - 1; x <= ex; x++ {
if y == p.Pos.Y && x == p.Pos.X {
continue
}
fp := g.Level.FlagsAt(y, x)
if fp.Has(FReal) {
continue
}
foundone := false
switch g.Level.Char(y, x) {
case '|', '-':
if g.rnd(5+probinc) != 0 {
break
}
g.Level.SetChar(y, x, Door)
g.msg("a secret door")
foundone = true
case Floor:
if g.rnd(2+probinc) != 0 {
break
}
g.Level.SetChar(y, x, Trap)
if !g.Options.Terse {
g.addmsg("you found ")
}
if p.On(Hallucinating) {
g.msg("%s", trName[g.rnd(NumTrapTypes)])
} else {
g.msg("%s", trName[*fp&FTrapMask])
fp.Set(FSeen)
}
foundone = true
case ' ':
if g.rnd(3+probinc) != 0 {
break
}
g.Level.SetChar(y, x, Passage)
foundone = true
}
if foundone {
found = true
fp.Set(FReal)
g.Count = 0
g.Running = false
}
}
}
if found {
g.look(false)
}
}
// help gives single character help, or the whole mess if he wants it
// (command.c help).
func (g *RogueGame) help() {
g.msg("character you want help for (* for all): ")
helpch := g.readchar()
g.Msgs.Mpos = 0
// If it's not a *, print the right help string or an error if he
// typed a funny character.
if helpch != '*' {
g.move(0, 0)
for _, strp := range helpStr {
if strp.Ch == helpch {
g.Msgs.LowerMsg = true
g.msg("%s%s", unctrl(strp.Ch), strp.Desc)
g.Msgs.LowerMsg = false
return
}
}
g.msg("unknown character '%s'", unctrl(helpch))
return
}
// Here we print help for everything, then wait before we return to
// command mode.
numprint := 0
for _, strp := range helpStr {
if strp.Print {
numprint++
}
}
if numprint&1 == 1 { // round odd numbers up
numprint++
}
numprint /= 2
if numprint > NumLines-1 {
numprint = NumLines - 1
}
hw := g.scr.Hw
hw.Clear()
cnt := 0
for _, strp := range helpStr {
if !strp.Print {
continue
}
x := 0
if cnt >= numprint {
x = NumCols / 2
}
hw.Move(cnt%numprint, x)
if strp.Ch != 0 {
hw.AddStr(unctrl(strp.Ch))
}
hw.AddStr(strp.Desc)
if cnt++; cnt >= numprint*2 {
break
}
}
hw.MvAddStr(NumLines-1, 0, "--Press space to continue--")
g.scr.RefreshWin(hw)
g.waitFor(' ')
g.msg("")
g.refresh()
}
// identList is command.c's static ident_list.
var identList = []helpEntry{
{'|', "wall of a room", false},
{'-', "wall of a room", false},
{Gold, "gold", false},
{Stairs, "a staircase", false},
{Door, "door", false},
{Floor, "room floor", false},
{PlayerCh, "you", false},
{Passage, "passage", false},
{Trap, "trap", false},
{Potion, "potion", false},
{Scroll, "scroll", false},
{Food, "food", false},
{Weapon, "weapon", false},
{' ', "solid rock", false},
{Armor, "armor", false},
{Amulet, "the Amulet of Yendor", false},
{Ring, "ring", false},
{Stick, "wand or staff", false},
}
// identify tells the player what a certain thing is (command.c identify).
func (g *RogueGame) identify() {
g.msg("what do you want identified? ")
ch := g.readchar()
g.Msgs.Mpos = 0
if ch == Escape {
g.msg("")
return
}
var str string
if isUpper(ch) {
str = g.Monsters[ch-'A'].Name
} else {
str = "unknown character"
for _, hp := range identList {
if hp.Ch == ch {
str = hp.Desc
break
}
}
}
g.msg("'%s': %s", unctrl(ch), str)
}
// dLevel: he wants to go down a level (command.c d_level).
func (g *RogueGame) dLevel() {
if g.levitCheck() {
return
}
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) != Stairs {
g.msg("I see no way down")
} else {
g.Depth++
g.SeenStairs = false
g.NewLevel()
}
}
// uLevel: he wants to go up a level (command.c u_level).
func (g *RogueGame) uLevel() {
if g.levitCheck() {
return
}
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) == Stairs {
if g.HasAmulet {
g.Depth--
if g.Depth == 0 {
g.totalWinner()
}
g.NewLevel()
g.msg("you feel a wrenching sensation in your gut")
} else {
g.msg("your way is magically blocked")
}
} else {
g.msg("I see no way up")
}
}
// levitCheck checks whether she's levitating, and if she is, prints an
// appropriate message (command.c levit_check).
func (g *RogueGame) levitCheck() bool {
if !g.Player.On(Levitating) {
return false
}
g.msg("You can't. You're floating off the ground!")
return true
}
// call allows a user to call a potion, scroll, or ring something
// (command.c call).
func (g *RogueGame) call() {
obj := g.getItem("call", Callable)
// Make certain that it is something that we want to name
if obj == nil {
return
}
var op *ObjInfo
var elsewise string
var know *bool
var guess *string
it := &g.Items
switch obj.Type {
case Ring:
op = &it.Rings[obj.Which]
elsewise = it.RingStones[obj.Which]
case Potion:
op = &it.Potions[obj.Which]
elsewise = it.PotColors[obj.Which]
case Scroll:
op = &it.Scrolls[obj.Which]
elsewise = it.ScrNames[obj.Which]
case Stick:
op = &it.Sticks[obj.Which]
elsewise = it.WandMade[obj.Which]
case Food:
g.msg("you can't call that anything")
return
default:
guess = &obj.Label
elsewise = obj.Label
}
fromGuess := false
if op != nil {
know = &op.Know
guess = &op.Guess
if *guess != "" {
elsewise = *guess
fromGuess = true
}
} else {
fromGuess = elsewise != "" && elsewise == *guess
}
if know != nil && *know {
g.msg("that has already been identified")
return
}
if fromGuess {
if !g.Options.Terse {
g.addmsg("Was ")
}
g.msg("called \"%s\"", elsewise)
}
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
buf := elsewise
if g.getStr(&buf, g.scr.Std) == Norm {
*guess = buf
}
}
// current prints the current weapon/armor (command.c current).
func (g *RogueGame) current(cur *Object, how, where string) {
g.After = false
if cur != nil {
if !g.Options.Terse {
g.addmsg("you are %s (", how)
}
g.InvDescribe = false
g.addmsg("%c) %s", cur.PackCh, g.invName(cur, true))
g.InvDescribe = true
if where != "" {
g.addmsg(" %s", where)
}
g.endmsg()
} else {
if !g.Options.Terse {
g.addmsg("you are ")
}
g.addmsg("%s nothing", how)
if where != "" {
g.addmsg(" %s", where)
}
g.endmsg()
}
}
// shell lets them escape for a while (main.c shell). The terminal decides
// how to actually suspend; headless terminals just decline.
func (g *RogueGame) shell() {
g.After = false
if se, ok := g.scr.term.(interface{ ShellEscape() }); ok {
g.InShell = true
se.ShellEscape()
g.InShell = false
g.refresh()
} else {
g.msg("shell escape is not available")
}
}