go: port dungeon generation, items base, pack, and monster creation
rooms.c, passages.c, new_level.c ported in full: room/maze layout, corridor spanning tree with extra connections, passage numbering flood fill, trap/stairs/object placement, treasure rooms. Careful RNG-call ordering throughout keeps generation seed-faithful to C. Supporting subsystems this depends on, also ported: - screen.go: curses replaced by in-memory Window cell buffers behind a Terminal interface (tcell arrives with the UI phase; tests run headless) - io.go: msg/addmsg/endmsg message-line protocol, status line, step_ok - pack.c in full (add_pack sorting/stacking walk, get_item, inventory) - things.c in full (inv_name, new_thing, discovery lists, pagination) - monsters.c in full; chase.c navigation half (roomin/cansee/see_monst/ find_dest/runto/set_oldch); rings.c, armor.c in full - weapons.c/sticks.c creation half (init_weapon, fix_stick, num) - misc.c look() display update, eat, level-up, direction input - daemons.c callbacks except stomach (needs death()) and the runners chase driver (combat phase) - init.c init_player with the starting kit Tests: level invariants across seeds, determinism, 30-depth sweep.
This commit is contained in:
66
game/armor.go
Normal file
66
game/armor.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package game
|
||||
|
||||
// armor.c — misc functions for dealing with armor.
|
||||
|
||||
// wear lets the player put armor on (armor.c wear).
|
||||
func (g *RogueGame) wear() {
|
||||
p := &g.Player
|
||||
obj := g.getItem("wear", int(Armor))
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if p.CurArmor != nil {
|
||||
g.addmsg("you are already wearing some")
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(". You'll have to take it off first")
|
||||
}
|
||||
g.endmsg()
|
||||
g.After = false
|
||||
return
|
||||
}
|
||||
if obj.Type != Armor {
|
||||
g.msg("you can't wear that")
|
||||
return
|
||||
}
|
||||
g.wasteTime()
|
||||
obj.Flags.Set(IsKnow)
|
||||
sp := g.invName(obj, true)
|
||||
p.CurArmor = obj
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are now ")
|
||||
}
|
||||
g.msg("wearing %s", sp)
|
||||
}
|
||||
|
||||
// takeOff gets the armor off of the player's back (armor.c take_off).
|
||||
func (g *RogueGame) takeOff() {
|
||||
p := &g.Player
|
||||
obj := p.CurArmor
|
||||
if obj == nil {
|
||||
g.After = false
|
||||
if g.Options.Terse {
|
||||
g.msg("not wearing armor")
|
||||
} else {
|
||||
g.msg("you aren't wearing any armor")
|
||||
}
|
||||
return
|
||||
}
|
||||
if !g.dropCheck(p.CurArmor) {
|
||||
return
|
||||
}
|
||||
p.CurArmor = nil
|
||||
if g.Options.Terse {
|
||||
g.addmsg("was")
|
||||
} else {
|
||||
g.addmsg("you used to be")
|
||||
}
|
||||
g.msg(" wearing %c) %s", obj.PackCh, g.invName(obj, true))
|
||||
}
|
||||
|
||||
// wasteTime does nothing but let other things happen (armor.c waste_time).
|
||||
func (g *RogueGame) wasteTime() {
|
||||
g.DoDaemons(Before)
|
||||
g.DoFuses(Before)
|
||||
g.DoDaemons(After)
|
||||
g.DoFuses(After)
|
||||
}
|
||||
138
game/chase.go
Normal file
138
game/chase.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package game
|
||||
|
||||
// chase.c — the navigation and visibility half. The chase driver (runners,
|
||||
// move_monst, do_chase, chase) arrives with the combat phase, since it
|
||||
// attacks the player and fires dragon bolts.
|
||||
|
||||
// setOldch sets the oldch character for the monster (chase.c set_oldch).
|
||||
func (g *RogueGame) setOldch(tp *Monster, cp Coord) {
|
||||
if tp.Pos == cp {
|
||||
return
|
||||
}
|
||||
sch := tp.OldCh
|
||||
tp.OldCh = g.mvinch(cp.Y, cp.X)
|
||||
if !g.Player.On(IsBlind) {
|
||||
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(IsDark) {
|
||||
tp.OldCh = ' '
|
||||
} else if distCp(cp, g.Player.Pos) <= LampDist && g.Options.SeeFloor {
|
||||
tp.OldCh = g.Level.Char(cp.Y, cp.X)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seeMonst returns true if the hero can see the monster (chase.c
|
||||
// see_monst).
|
||||
func (g *RogueGame) seeMonst(mp *Monster) bool {
|
||||
p := &g.Player
|
||||
if p.On(IsBlind) {
|
||||
return false
|
||||
}
|
||||
if mp.On(IsInvis) && !p.On(CanSee) {
|
||||
return false
|
||||
}
|
||||
y, x := mp.Pos.Y, mp.Pos.X
|
||||
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
if y != p.Pos.Y && x != p.Pos.X &&
|
||||
!stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if mp.Room != p.Room {
|
||||
return false
|
||||
}
|
||||
return !mp.Room.Flags.Has(IsDark)
|
||||
}
|
||||
|
||||
// runto sets a monster running after the hero (chase.c runto).
|
||||
func (g *RogueGame) runto(runner Coord) {
|
||||
tp := g.Level.MonsterAt(runner.Y, runner.X)
|
||||
if tp == nil {
|
||||
return
|
||||
}
|
||||
// Start the beastie running
|
||||
tp.Flags.Set(IsRun)
|
||||
tp.Flags.Clear(IsHeld)
|
||||
tp.Dest = g.findDest(tp)
|
||||
}
|
||||
|
||||
// roomin finds what room some coordinates are in; nil means they aren't in
|
||||
// any room (chase.c roomin).
|
||||
func (g *RogueGame) roomin(cp Coord) *Room {
|
||||
fp := *g.Level.FlagsAt(cp.Y, cp.X)
|
||||
if fp.Has(FPass) {
|
||||
return &g.Level.Passages[fp&FPNum]
|
||||
}
|
||||
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
if cp.X <= rp.Pos.X+rp.Max.X && rp.Pos.X <= cp.X &&
|
||||
cp.Y <= rp.Pos.Y+rp.Max.Y && rp.Pos.Y <= cp.Y {
|
||||
return rp
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
|
||||
return nil
|
||||
}
|
||||
|
||||
// diagOk checks to see if a diagonal move is legal (chase.c diag_ok).
|
||||
func (g *RogueGame) diagOk(sp, ep Coord) bool {
|
||||
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
|
||||
return false
|
||||
}
|
||||
if ep.X == sp.X || ep.Y == sp.Y {
|
||||
return true
|
||||
}
|
||||
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
|
||||
}
|
||||
|
||||
// cansee returns true if the hero can see a certain coordinate (chase.c
|
||||
// cansee).
|
||||
func (g *RogueGame) cansee(y, x int) bool {
|
||||
p := &g.Player
|
||||
if p.On(IsBlind) {
|
||||
return false
|
||||
}
|
||||
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
if g.Level.FlagsAt(y, x).Has(FPass) {
|
||||
if y != p.Pos.Y && x != p.Pos.X &&
|
||||
!stepOk(g.Level.Char(y, p.Pos.X)) &&
|
||||
!stepOk(g.Level.Char(p.Pos.Y, x)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
// We can only see if the hero is in the same room as the coordinate
|
||||
// and the room is lit, or if it is close.
|
||||
rer := g.roomin(Coord{X: x, Y: y})
|
||||
return rer == p.Room && !rer.Flags.Has(IsDark)
|
||||
}
|
||||
|
||||
// findDest finds the proper destination for the monster (chase.c
|
||||
// find_dest).
|
||||
func (g *RogueGame) findDest(tp *Monster) *Coord {
|
||||
prob := monsterTable[tp.Type-'A'].Carry
|
||||
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
|
||||
return &g.Player.Pos
|
||||
}
|
||||
for _, obj := range g.Level.Objects {
|
||||
if obj.Type == Scroll && obj.Which == SScare {
|
||||
continue
|
||||
}
|
||||
if g.roomin(obj.Pos) == tp.Room && g.rnd(100) < prob {
|
||||
claimed := false
|
||||
for _, other := range g.Level.Monsters {
|
||||
if other.Dest == &obj.Pos {
|
||||
claimed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !claimed {
|
||||
return &obj.Pos
|
||||
}
|
||||
}
|
||||
}
|
||||
return &g.Player.Pos
|
||||
}
|
||||
200
game/daemons.go
200
game/daemons.go
@@ -1,17 +1,205 @@
|
||||
package game
|
||||
|
||||
// daemons.c — the daemon and fuse callbacks. Each callback is ported in the
|
||||
// phase that owns its subsystem; runDaemon is the dispatch switch that
|
||||
// replaces the C function pointers.
|
||||
// daemons.c — the daemon and fuse callbacks, dispatched by DaemonID.
|
||||
// stomach() arrives with the endgame phase (starvation calls death()).
|
||||
|
||||
// runDaemon invokes the callback named by id (the call through d_func in C).
|
||||
func (g *RogueGame) runDaemon(id DaemonID, arg int) {
|
||||
_ = arg
|
||||
switch id {
|
||||
case DRollwand:
|
||||
g.rollwand(arg)
|
||||
case DDoctor:
|
||||
g.doctor(arg)
|
||||
case DSwander:
|
||||
g.swander(arg)
|
||||
case DNohaste:
|
||||
g.nohaste(arg)
|
||||
case DUnconfuse:
|
||||
g.unconfuse(arg)
|
||||
case DUnsee:
|
||||
g.unsee(arg)
|
||||
case DSight:
|
||||
g.sight(arg)
|
||||
case DVisuals:
|
||||
g.visuals(arg)
|
||||
case DComeDown:
|
||||
g.comeDown(arg)
|
||||
case DLand:
|
||||
g.land(arg)
|
||||
default:
|
||||
// Callbacks are added to this switch as their subsystems are
|
||||
// ported; reaching one that isn't here is a porting bug, not a
|
||||
// game state.
|
||||
// ported; reaching one that isn't here is a porting bug.
|
||||
panic("daemon not yet ported")
|
||||
}
|
||||
}
|
||||
|
||||
// doctor is the healing daemon that restores hit points after rest
|
||||
// (daemons.c doctor).
|
||||
func (g *RogueGame) doctor(int) {
|
||||
p := &g.Player
|
||||
lv := p.Stats.Lvl
|
||||
ohp := p.Stats.HP
|
||||
g.Quiet++
|
||||
if lv < 8 {
|
||||
if g.Quiet+(lv<<1) > 20 {
|
||||
p.Stats.HP++
|
||||
}
|
||||
} else if g.Quiet >= 3 {
|
||||
p.Stats.HP += g.rnd(lv-7) + 1
|
||||
}
|
||||
if p.IsRing(Left, RRegen) {
|
||||
p.Stats.HP++
|
||||
}
|
||||
if p.IsRing(Right, RRegen) {
|
||||
p.Stats.HP++
|
||||
}
|
||||
if ohp != p.Stats.HP {
|
||||
if p.Stats.HP > p.Stats.MaxHP {
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
g.Quiet = 0
|
||||
}
|
||||
}
|
||||
|
||||
// swander is called when it is time to start rolling for wandering monsters
|
||||
// (daemons.c swander).
|
||||
func (g *RogueGame) swander(int) {
|
||||
g.StartDaemon(DRollwand, 0, Before)
|
||||
}
|
||||
|
||||
// rollwand rolls to see if a wandering monster starts up (daemons.c
|
||||
// rollwand).
|
||||
func (g *RogueGame) rollwand(int) {
|
||||
if g.Daemons.Between++; g.Daemons.Between >= 4 {
|
||||
if g.roll(1, 6) == 4 {
|
||||
g.wanderer()
|
||||
g.KillDaemon(DRollwand)
|
||||
g.Fuse(DSwander, 0, wanderTime(g), Before)
|
||||
}
|
||||
g.Daemons.Between = 0
|
||||
}
|
||||
}
|
||||
|
||||
// wanderTime is the WANDERTIME macro: spread(70).
|
||||
func wanderTime(g *RogueGame) int { return g.spread(70) }
|
||||
|
||||
// unconfuse releases the poor player from his confusion (daemons.c
|
||||
// unconfuse).
|
||||
func (g *RogueGame) unconfuse(int) {
|
||||
g.Player.Flags.Clear(IsHuh)
|
||||
g.msg("you feel less %s now", g.chooseStr("trippy", "confused"))
|
||||
}
|
||||
|
||||
// unsee turns off the ability to see invisible (daemons.c unsee).
|
||||
func (g *RogueGame) unsee(int) {
|
||||
for _, th := range g.Level.Monsters {
|
||||
if th.On(IsInvis) && g.seeMonst(th) {
|
||||
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
|
||||
}
|
||||
}
|
||||
g.Player.Flags.Clear(CanSee)
|
||||
}
|
||||
|
||||
// sight gives the hero his sight back (daemons.c sight).
|
||||
func (g *RogueGame) sight(int) {
|
||||
p := &g.Player
|
||||
if p.On(IsBlind) {
|
||||
g.Extinguish(DSight)
|
||||
p.Flags.Clear(IsBlind)
|
||||
if !p.Room.Flags.Has(IsGone) {
|
||||
g.enterRoom(p.Pos)
|
||||
}
|
||||
g.msg("%s", g.chooseStr("far out! Everything is all cosmic again",
|
||||
"the veil of darkness lifts"))
|
||||
}
|
||||
}
|
||||
|
||||
// nohaste ends the hasting (daemons.c nohaste).
|
||||
func (g *RogueGame) nohaste(int) {
|
||||
g.Player.Flags.Clear(IsHaste)
|
||||
g.msg("you feel yourself slowing down")
|
||||
}
|
||||
|
||||
// comeDown takes the hero down off her acid trip (daemons.c come_down).
|
||||
func (g *RogueGame) comeDown(int) {
|
||||
p := &g.Player
|
||||
if !p.On(IsHalu) {
|
||||
return
|
||||
}
|
||||
|
||||
g.KillDaemon(DVisuals)
|
||||
p.Flags.Clear(IsHalu)
|
||||
|
||||
if p.On(IsBlind) {
|
||||
return
|
||||
}
|
||||
|
||||
// undo the things
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.cansee(tp.Pos.Y, tp.Pos.X) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// undo the monsters
|
||||
seemonst := p.On(SeeMonst)
|
||||
for _, tp := range g.Level.Monsters {
|
||||
g.move(tp.Pos.Y, tp.Pos.X)
|
||||
if g.cansee(tp.Pos.Y, tp.Pos.X) {
|
||||
if !tp.On(IsInvis) || p.On(CanSee) {
|
||||
g.addch(tp.Disguise)
|
||||
} else {
|
||||
g.addch(g.Level.Char(tp.Pos.Y, tp.Pos.X))
|
||||
}
|
||||
} else if seemonst {
|
||||
g.standout()
|
||||
g.addch(tp.Type)
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
g.msg("Everything looks SO boring now.")
|
||||
}
|
||||
|
||||
// visuals changes the characters for the player while hallucinating
|
||||
// (daemons.c visuals).
|
||||
func (g *RogueGame) visuals(int) {
|
||||
p := &g.Player
|
||||
if !g.After || (g.Running && g.Options.Jump) {
|
||||
return
|
||||
}
|
||||
// change the things
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.cansee(tp.Pos.Y, tp.Pos.X) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing())
|
||||
}
|
||||
}
|
||||
|
||||
// change the stairs
|
||||
if !g.SeenStairs && g.cansee(g.Level.Stairs.Y, g.Level.Stairs.X) {
|
||||
g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing())
|
||||
}
|
||||
|
||||
// change the monsters
|
||||
seemonst := p.On(SeeMonst)
|
||||
for _, tp := range g.Level.Monsters {
|
||||
g.move(tp.Pos.Y, tp.Pos.X)
|
||||
if g.seeMonst(tp) {
|
||||
if tp.Type == 'X' && tp.Disguise != 'X' {
|
||||
g.addch(g.rndThing())
|
||||
} else {
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
}
|
||||
} else if seemonst {
|
||||
g.standout()
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// land lands the hero from a levitation potion (daemons.c land).
|
||||
func (g *RogueGame) land(int) {
|
||||
g.Player.Flags.Clear(IsLevit)
|
||||
g.msg("%s", g.chooseStr("bummer! You've hit the ground",
|
||||
"you float gently to the ground"))
|
||||
}
|
||||
|
||||
28
game/fight.go
Normal file
28
game/fight.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package game
|
||||
|
||||
// fight.c — combat. setMname arrives first (monster wake-ups need it); the
|
||||
// combat resolution functions come with the combat phase.
|
||||
|
||||
// setMname returns the monster name for the given monster (fight.c
|
||||
// set_mname).
|
||||
func (g *RogueGame) setMname(tp *Monster) string {
|
||||
if !g.seeMonst(tp) && !g.Player.On(SeeMonst) {
|
||||
if g.Options.Terse {
|
||||
return "it"
|
||||
}
|
||||
return "something"
|
||||
}
|
||||
var mname string
|
||||
if g.Player.On(IsHalu) {
|
||||
ch := int(g.mvinch(tp.Pos.Y, tp.Pos.X))
|
||||
if !isUpper(byte(ch)) {
|
||||
ch = g.rnd(26)
|
||||
} else {
|
||||
ch -= 'A'
|
||||
}
|
||||
mname = monsterTable[ch].Name
|
||||
} else {
|
||||
mname = monsterTable[tp.Type-'A'].Name
|
||||
}
|
||||
return "the " + mname
|
||||
}
|
||||
73
game/game.go
73
game/game.go
@@ -33,12 +33,13 @@ type Options struct {
|
||||
|
||||
// Config carries everything needed to construct a game.
|
||||
type Config struct {
|
||||
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env)
|
||||
Name string // player name (overridden by ROGUEOPTS name=)
|
||||
RogueOpts string // the ROGUEOPTS environment string
|
||||
Home string // home directory (save file default location)
|
||||
ScorePath string // scoreboard file; empty disables scoring
|
||||
Wizard bool // enable debug commands (implies NoScore)
|
||||
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env)
|
||||
Name string // player name (overridden by ROGUEOPTS name=)
|
||||
RogueOpts string // the ROGUEOPTS environment string
|
||||
Home string // home directory (save file default location)
|
||||
ScorePath string // scoreboard file; empty disables scoring
|
||||
Wizard bool // enable debug commands (implies NoScore)
|
||||
Term Terminal // the display/input device; nil runs headless
|
||||
}
|
||||
|
||||
// RogueGame is one complete game of Rogue: every piece of state that was a
|
||||
@@ -92,11 +93,23 @@ type RogueGame struct {
|
||||
LLastDir byte
|
||||
LastPick *Object
|
||||
LLastPick *Object
|
||||
lastDelt Coord // misc.c get_dir static last_delt
|
||||
|
||||
// dungeon generation working state
|
||||
maze mazeState // rooms.c maze statics
|
||||
pnum int // passages.c passnum statics
|
||||
newpnum bool
|
||||
|
||||
// daemons/fuses
|
||||
Daemons DaemonList
|
||||
|
||||
// UI state (the Screen itself arrives with the UI phase)
|
||||
// screen / messages
|
||||
scr *Screen
|
||||
Msgs MsgLine
|
||||
statusCache statusCache
|
||||
invPage invPage // things.c discovery-list pagination statics
|
||||
|
||||
// UI state
|
||||
StatMsg bool // should status() print as a msg()
|
||||
InvDescribe bool // say which way items are being used
|
||||
QComm bool // are we executing a 'Q' command?
|
||||
@@ -138,18 +151,58 @@ func NewGame(cfg Config) *RogueGame {
|
||||
InvType: InvOver,
|
||||
}
|
||||
g.InvDescribe = true
|
||||
g.Msgs.SaveMsg = true
|
||||
g.scr = NewScreen(cfg.Term)
|
||||
g.FileName = cfg.Home + "/rogue.save"
|
||||
if cfg.Wizard {
|
||||
g.Player.Flags.Set(SeeMonst)
|
||||
}
|
||||
// TODO(port): parse cfg.RogueOpts once options.go lands.
|
||||
|
||||
g.initProbs() // set up prob tables for objects
|
||||
// TODO(port): initPlayer() goes exactly here (RNG order!) once pack.go lands.
|
||||
g.Items.Group = 2 // weapons.c: int group = 2
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Flags = IsGone | IsDark
|
||||
}
|
||||
|
||||
g.initProbs() // set up prob tables for objects
|
||||
g.initPlayer() // set up initial player stats
|
||||
g.initNames() // set up names of scrolls
|
||||
g.initColors() // set up colors of potions
|
||||
g.initStones() // set up stone settings of rings
|
||||
g.initMaterials() // set up materials of wands
|
||||
// TODO(port): new level + daemon start-up once their phases land.
|
||||
return g
|
||||
}
|
||||
|
||||
// quit has the player make certain, then exits (main.c quit). The final
|
||||
// scoring display arrives with the endgame phase.
|
||||
func (g *RogueGame) quit(int) {
|
||||
// Reset the message position in case we got here via an interrupt
|
||||
if !g.QComm {
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
oy, ox := g.scr.Std.GetYX()
|
||||
g.msg("really quit?")
|
||||
if g.readchar() == 'y' {
|
||||
g.clear()
|
||||
g.scr.Std.MvPrintw(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
|
||||
g.move(NumLines-1, 0)
|
||||
g.refresh()
|
||||
// TODO(port): score(purse, 1, 0) once rip.go lands.
|
||||
g.gameOver()
|
||||
return
|
||||
}
|
||||
g.move(0, 0)
|
||||
g.clrtoeol()
|
||||
g.status()
|
||||
g.move(oy, ox)
|
||||
g.refresh()
|
||||
g.Msgs.Mpos = 0
|
||||
g.Count = 0
|
||||
g.ToDeath = false
|
||||
}
|
||||
|
||||
// gameOver ends the game loop; it replaces the C exit() calls so that Run
|
||||
// can unwind normally and restore the terminal.
|
||||
func (g *RogueGame) gameOver() {
|
||||
g.Playing = false
|
||||
}
|
||||
|
||||
44
game/init.go
44
game/init.go
@@ -3,7 +3,49 @@ package game
|
||||
import "strings"
|
||||
|
||||
// init.c — per-game randomization of item appearances and probability
|
||||
// tables. initPlayer arrives with the pack/items phase (it needs addPack).
|
||||
// tables, and the player roll-up.
|
||||
|
||||
// initPlayer rolls her up (init.c init_player).
|
||||
func (g *RogueGame) initPlayer() {
|
||||
p := &g.Player
|
||||
p.MaxStats = initStats
|
||||
p.Stats = p.MaxStats
|
||||
p.FoodLeft = HungerTime
|
||||
// Give him some food
|
||||
obj := newObject()
|
||||
obj.Type = Food
|
||||
obj.Count = 1
|
||||
g.addPack(obj, true)
|
||||
// And his suit of armor
|
||||
obj = newObject()
|
||||
obj.Type = Armor
|
||||
obj.Which = RingMail
|
||||
obj.Arm = aClass[RingMail] - 1
|
||||
obj.Flags.Set(IsKnow)
|
||||
obj.Count = 1
|
||||
p.CurArmor = obj
|
||||
g.addPack(obj, true)
|
||||
// Give him his weaponry. First a mace.
|
||||
obj = newObject()
|
||||
g.initWeapon(obj, Mace)
|
||||
obj.HPlus = 1
|
||||
obj.DPlus = 1
|
||||
obj.Flags.Set(IsKnow)
|
||||
g.addPack(obj, true)
|
||||
p.CurWeapon = obj
|
||||
// Now a +1 bow
|
||||
obj = newObject()
|
||||
g.initWeapon(obj, Bow)
|
||||
obj.HPlus = 1
|
||||
obj.Flags.Set(IsKnow)
|
||||
g.addPack(obj, true)
|
||||
// Now some arrows
|
||||
obj = newObject()
|
||||
g.initWeapon(obj, Arrow)
|
||||
obj.Count = g.rnd(15) + 25
|
||||
obj.Flags.Set(IsKnow)
|
||||
g.addPack(obj, true)
|
||||
}
|
||||
|
||||
// initColors initializes the potion color scheme for this game
|
||||
// (init.c init_colors).
|
||||
|
||||
231
game/io.go
Normal file
231
game/io.go
Normal file
@@ -0,0 +1,231 @@
|
||||
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
|
||||
|
||||
// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus
|
||||
// the related globals (mpos, huh, and the message-behavior flags).
|
||||
type MsgLine 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--
|
||||
}
|
||||
|
||||
// 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 (g *RogueGame) msg(format string, a ...any) int {
|
||||
// if the string is "", just clear the line
|
||||
if format == "" {
|
||||
g.move(0, 0)
|
||||
g.clrtoeol()
|
||||
g.Msgs.Mpos = 0
|
||||
return ^Escape
|
||||
}
|
||||
// otherwise add to the message and flush it out
|
||||
g.doadd(format, a...)
|
||||
return g.endmsg()
|
||||
}
|
||||
|
||||
// addmsg adds things to the current message (io.c addmsg).
|
||||
func (g *RogueGame) addmsg(format string, a ...any) {
|
||||
g.doadd(format, a...)
|
||||
}
|
||||
|
||||
// endmsg 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 (g *RogueGame) endmsg() int {
|
||||
m := &g.Msgs
|
||||
if m.SaveMsg {
|
||||
m.Huh = m.buf.String()
|
||||
}
|
||||
if m.Mpos != 0 {
|
||||
g.look(false)
|
||||
g.mvaddstr(0, m.Mpos, "--More--")
|
||||
g.refresh()
|
||||
if !m.MsgEsc {
|
||||
g.waitFor(' ')
|
||||
} else {
|
||||
for {
|
||||
ch := g.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:]
|
||||
}
|
||||
g.mvaddstr(0, 0, out)
|
||||
g.clrtoeol()
|
||||
m.Mpos = m.newpos
|
||||
m.newpos = 0
|
||||
m.buf.Reset()
|
||||
g.refresh()
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// doadd performs an add onto the message buffer (io.c doadd).
|
||||
func (g *RogueGame) doadd(format string, a ...any) {
|
||||
m := &g.Msgs
|
||||
s := fmt.Sprintf(format, a...)
|
||||
if len(s)+m.newpos >= maxMsg {
|
||||
g.endmsg()
|
||||
}
|
||||
m.buf.WriteString(s)
|
||||
m.newpos = m.buf.Len()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"}
|
||||
|
||||
// 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.Arm
|
||||
if p.CurArmor != nil {
|
||||
temp = p.CurArmor.Arm
|
||||
}
|
||||
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,
|
||||
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 g.readchar() != ch {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 <ctype.h>; 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
|
||||
}
|
||||
455
game/misc.go
Normal file
455
game/misc.go
Normal file
@@ -0,0 +1,455 @@
|
||||
package game
|
||||
|
||||
// misc.c — look() display maintenance, direction input, eating, level-ups,
|
||||
// and small utilities. call_it arrives with the scroll/potion phase (it
|
||||
// needs the get_str line editor).
|
||||
|
||||
// look takes a quick glance all around the player (misc.c look).
|
||||
func (g *RogueGame) look(wakeup bool) {
|
||||
p := &g.Player
|
||||
hero := p.Pos
|
||||
passcount := 0
|
||||
rp := p.Room
|
||||
if g.Oldpos != hero {
|
||||
g.eraseLamp(g.Oldpos, g.Oldrp)
|
||||
g.Oldpos = hero
|
||||
g.Oldrp = rp
|
||||
}
|
||||
ey := hero.Y + 1
|
||||
ex := hero.X + 1
|
||||
sx := hero.X - 1
|
||||
sy := hero.Y - 1
|
||||
sumhero, diffhero := 0, 0
|
||||
if g.DoorStop && !g.Firstmove && g.Running {
|
||||
sumhero = hero.Y + hero.X
|
||||
diffhero = hero.Y - hero.X
|
||||
}
|
||||
pp := g.Level.At(hero.Y, hero.X)
|
||||
pch := pp.Ch
|
||||
pfl := pp.Flags
|
||||
|
||||
for y := sy; y <= ey; y++ {
|
||||
if y <= 0 || y >= NumLines-1 {
|
||||
continue
|
||||
}
|
||||
for x := sx; x <= ex; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
if !p.On(IsBlind) {
|
||||
if y == hero.Y && x == hero.X {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
ch := pp.Ch
|
||||
if ch == ' ' { // nothing need be done with a ' '
|
||||
continue
|
||||
}
|
||||
fp := &pp.Flags
|
||||
if pch != Door && ch != Door {
|
||||
if (pfl & FPass) != (*fp & FPass) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (fp.Has(FPass) || ch == Door) && (pfl.Has(FPass) || pch == Door) {
|
||||
if hero.X != x && hero.Y != y &&
|
||||
!stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
tp := pp.Monst
|
||||
if tp == nil {
|
||||
ch = g.tripCh(y, x, ch)
|
||||
} else if p.On(SeeMonst) && tp.On(IsInvis) {
|
||||
if g.DoorStop && !g.Firstmove {
|
||||
g.Running = false
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
if wakeup {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
if g.seeMonst(tp) {
|
||||
if p.On(IsHalu) {
|
||||
ch = byte(g.rnd(26) + 'A')
|
||||
} else {
|
||||
ch = tp.Disguise
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.On(IsBlind) && (y != hero.Y || x != hero.X) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.move(y, x)
|
||||
|
||||
if p.Room.Flags.Has(IsDark) && !g.Options.SeeFloor && ch == Floor {
|
||||
ch = ' '
|
||||
}
|
||||
|
||||
if tp != nil || ch != g.inch() {
|
||||
g.addch(ch)
|
||||
}
|
||||
|
||||
if g.DoorStop && !g.Firstmove && g.Running {
|
||||
switch g.RunCh {
|
||||
case 'h':
|
||||
if x == ex {
|
||||
continue
|
||||
}
|
||||
case 'j':
|
||||
if y == sy {
|
||||
continue
|
||||
}
|
||||
case 'k':
|
||||
if y == ey {
|
||||
continue
|
||||
}
|
||||
case 'l':
|
||||
if x == sx {
|
||||
continue
|
||||
}
|
||||
case 'y':
|
||||
if (y+x)-sumhero >= 1 {
|
||||
continue
|
||||
}
|
||||
case 'u':
|
||||
if (y-x)-diffhero >= 1 {
|
||||
continue
|
||||
}
|
||||
case 'n':
|
||||
if (y+x)-sumhero <= -1 {
|
||||
continue
|
||||
}
|
||||
case 'b':
|
||||
if (y-x)-diffhero <= -1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch ch {
|
||||
case Door:
|
||||
if x == hero.X || y == hero.Y {
|
||||
g.Running = false
|
||||
}
|
||||
case Passage:
|
||||
if x == hero.X || y == hero.Y {
|
||||
passcount++
|
||||
}
|
||||
case Floor, '|', '-', ' ':
|
||||
default:
|
||||
g.Running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if g.DoorStop && !g.Firstmove && passcount > 1 {
|
||||
g.Running = false
|
||||
}
|
||||
if !g.Running || !g.Options.Jump {
|
||||
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
||||
}
|
||||
}
|
||||
|
||||
// tripCh returns the character for this space, taking into account whether
|
||||
// or not the player is tripping (misc.c trip_ch).
|
||||
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
|
||||
if g.Player.On(IsHalu) && g.After {
|
||||
switch ch {
|
||||
case Floor, ' ', Passage, '-', '|', Door, Trap:
|
||||
default:
|
||||
if y != g.Level.Stairs.Y || x != g.Level.Stairs.X || !g.SeenStairs {
|
||||
ch = g.rndThing()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// eraseLamp erases the area shown by a lamp in a dark room
|
||||
// (misc.c erase_lamp).
|
||||
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
|
||||
if !(g.Options.SeeFloor && rp.Flags&(IsGone|IsDark) == IsDark &&
|
||||
!g.Player.On(IsBlind)) {
|
||||
return
|
||||
}
|
||||
|
||||
ey := pos.Y + 1
|
||||
ex := pos.X + 1
|
||||
sy := pos.Y - 1
|
||||
for x := pos.X - 1; x <= ex; x++ {
|
||||
for y := sy; y <= ey; y++ {
|
||||
if y == g.Player.Pos.Y && x == g.Player.Pos.X {
|
||||
continue
|
||||
}
|
||||
g.move(y, x)
|
||||
if g.inch() == Floor {
|
||||
g.addch(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// showFloor reports whether we show the floor in her room at this time
|
||||
// (misc.c show_floor).
|
||||
func (g *RogueGame) showFloor() bool {
|
||||
if g.Player.Room.Flags&(IsGone|IsDark) == IsDark && !g.Player.On(IsBlind) {
|
||||
return g.Options.SeeFloor
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// findObj finds the unclaimed object at (y, x) (misc.c find_obj).
|
||||
func (g *RogueGame) findObj(y, x int) *Object {
|
||||
for _, obj := range g.Level.Objects {
|
||||
if obj.Pos.Y == y && obj.Pos.X == x {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// eat lets her try to eat something (misc.c eat).
|
||||
func (g *RogueGame) eat() {
|
||||
obj := g.getItem("eat", int(Food))
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if obj.Type != Food {
|
||||
if !g.Options.Terse {
|
||||
g.msg("ugh, you would get ill if you ate that")
|
||||
} else {
|
||||
g.msg("that's Inedible!")
|
||||
}
|
||||
return
|
||||
}
|
||||
p := &g.Player
|
||||
if p.FoodLeft < 0 {
|
||||
p.FoodLeft = 0
|
||||
}
|
||||
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
|
||||
p.FoodLeft = StomachSize
|
||||
}
|
||||
p.HungryState = 0
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
}
|
||||
if obj.Which == 1 {
|
||||
g.msg("my, that was a yummy %s", g.Fruit)
|
||||
} else if g.rnd(100) > 70 {
|
||||
p.Stats.Exp++
|
||||
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
|
||||
g.checkLevel()
|
||||
} else {
|
||||
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
|
||||
}
|
||||
g.leavePack(obj, false, false)
|
||||
}
|
||||
|
||||
// checkLevel checks to see if the guy has gone up a level (misc.c
|
||||
// check_level).
|
||||
func (g *RogueGame) checkLevel() {
|
||||
p := &g.Player
|
||||
var i int
|
||||
for i = 0; eLevels[i] != 0; i++ {
|
||||
if eLevels[i] > p.Stats.Exp {
|
||||
break
|
||||
}
|
||||
}
|
||||
i++
|
||||
olevel := p.Stats.Lvl
|
||||
p.Stats.Lvl = i
|
||||
if i > olevel {
|
||||
add := g.roll(i-olevel, 10)
|
||||
p.Stats.MaxHP += add
|
||||
p.Stats.HP += add
|
||||
g.msg("welcome to level %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// chgStr modifies the player's strength, keeping track of the highest it
|
||||
// has been (misc.c chg_str).
|
||||
func (g *RogueGame) chgStr(amt int) {
|
||||
if amt == 0 {
|
||||
return
|
||||
}
|
||||
p := &g.Player
|
||||
addStr(&p.Stats.Str, amt)
|
||||
comp := p.Stats.Str
|
||||
if p.IsRing(Left, RAddStr) {
|
||||
addStr(&comp, -p.CurRing[Left].Arm)
|
||||
}
|
||||
if p.IsRing(Right, RAddStr) {
|
||||
addStr(&comp, -p.CurRing[Right].Arm)
|
||||
}
|
||||
if comp > p.MaxStats.Str {
|
||||
p.MaxStats.Str = comp
|
||||
}
|
||||
}
|
||||
|
||||
// addStr performs the actual strength add, checking bounds (misc.c add_str).
|
||||
func addStr(sp *int, amt int) {
|
||||
if *sp += amt; *sp < 3 {
|
||||
*sp = 3
|
||||
} else if *sp > 31 {
|
||||
*sp = 31
|
||||
}
|
||||
}
|
||||
|
||||
// addHaste adds a haste to the player (misc.c add_haste).
|
||||
func (g *RogueGame) addHaste(potion bool) bool {
|
||||
p := &g.Player
|
||||
if p.On(IsHaste) {
|
||||
g.NoCommand += g.rnd(8)
|
||||
p.Flags.Clear(IsRun | IsHaste)
|
||||
g.Extinguish(DNohaste)
|
||||
g.msg("you faint from exhaustion")
|
||||
return false
|
||||
}
|
||||
p.Flags.Set(IsHaste)
|
||||
if potion {
|
||||
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// aggravate aggravates all the monsters on this level (misc.c aggravate).
|
||||
func (g *RogueGame) aggravate() {
|
||||
// runto() can splice the monster list while we walk it, so iterate a copy.
|
||||
monsters := append([]*Monster(nil), g.Level.Monsters...)
|
||||
for _, mp := range monsters {
|
||||
g.runto(mp.Pos)
|
||||
}
|
||||
}
|
||||
|
||||
// vowelstr returns "n" if the string starts with a vowel, for "a"/"an"
|
||||
// (misc.c vowelstr).
|
||||
func vowelstr(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
switch str[0] {
|
||||
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
|
||||
return "n"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isCurrent sees if the object is one of the currently used items
|
||||
// (misc.c is_current).
|
||||
func (g *RogueGame) isCurrent(obj *Object) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor || obj == p.CurWeapon ||
|
||||
obj == p.CurRing[Left] || obj == p.CurRing[Right] {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("That's already ")
|
||||
}
|
||||
g.msg("in use")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getDir sets up the direction coordinate for use in various "prefix"
|
||||
// commands (misc.c get_dir).
|
||||
func (g *RogueGame) getDir() bool {
|
||||
if g.Again && g.LastDir != 0 {
|
||||
g.Delta = g.lastDelt
|
||||
g.DirCh = g.LastDir
|
||||
} else {
|
||||
prompt := "direction: "
|
||||
if !g.Options.Terse {
|
||||
prompt = "which direction? "
|
||||
g.msg("%s", prompt)
|
||||
}
|
||||
for {
|
||||
gotit := true
|
||||
switch g.DirCh = g.readchar(); g.DirCh {
|
||||
case 'h', 'H':
|
||||
g.Delta = Coord{X: -1, Y: 0}
|
||||
case 'j', 'J':
|
||||
g.Delta = Coord{X: 0, Y: 1}
|
||||
case 'k', 'K':
|
||||
g.Delta = Coord{X: 0, Y: -1}
|
||||
case 'l', 'L':
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
case 'y', 'Y':
|
||||
g.Delta = Coord{X: -1, Y: -1}
|
||||
case 'u', 'U':
|
||||
g.Delta = Coord{X: 1, Y: -1}
|
||||
case 'b', 'B':
|
||||
g.Delta = Coord{X: -1, Y: 1}
|
||||
case 'n', 'N':
|
||||
g.Delta = Coord{X: 1, Y: 1}
|
||||
case Escape:
|
||||
g.LastDir = 0
|
||||
g.resetLast()
|
||||
return false
|
||||
default:
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", prompt)
|
||||
gotit = false
|
||||
}
|
||||
if gotit {
|
||||
break
|
||||
}
|
||||
}
|
||||
g.DirCh = toLower(g.DirCh)
|
||||
g.LastDir = g.DirCh
|
||||
g.lastDelt = g.Delta
|
||||
}
|
||||
if g.Player.On(IsHuh) && g.rnd(5) == 0 {
|
||||
for {
|
||||
g.Delta.Y = g.rnd(3) - 1
|
||||
g.Delta.X = g.rnd(3) - 1
|
||||
if g.Delta.Y != 0 || g.Delta.X != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Msgs.Mpos = 0
|
||||
return true
|
||||
}
|
||||
|
||||
// thingList is misc.c rnd_thing()'s static table.
|
||||
var thingList = []byte{
|
||||
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
|
||||
}
|
||||
|
||||
// rndThing picks a random thing appropriate for this level (misc.c
|
||||
// rnd_thing).
|
||||
func (g *RogueGame) rndThing() byte {
|
||||
var i int
|
||||
if g.Depth >= AmuletLevel {
|
||||
i = g.rnd(len(thingList))
|
||||
} else {
|
||||
i = g.rnd(len(thingList) - 1)
|
||||
}
|
||||
return thingList[i]
|
||||
}
|
||||
|
||||
// chooseStr picks the first or second string depending on whether the
|
||||
// player is tripping (misc.c choose_str).
|
||||
func (g *RogueGame) chooseStr(ts, ns string) string {
|
||||
if g.Player.On(IsHalu) {
|
||||
return ts
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// unctrl gives a printable representation of a character, like curses
|
||||
// unctrl(): control characters display as ^X.
|
||||
func unctrl(ch byte) string {
|
||||
if ch < ' ' {
|
||||
return "^" + string(ch+'@')
|
||||
}
|
||||
if ch == 0x7f {
|
||||
return "^?"
|
||||
}
|
||||
return string(ch)
|
||||
}
|
||||
193
game/monsters.go
Normal file
193
game/monsters.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package game
|
||||
|
||||
// monsters.c — monster creation and saving throws.
|
||||
|
||||
// lvlMons and wandMons list monsters in rough order of vorpalness; zero
|
||||
// entries in wandMons never wander (monsters.c).
|
||||
var lvlMons = [26]byte{
|
||||
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
|
||||
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
|
||||
}
|
||||
|
||||
var wandMons = [26]byte{
|
||||
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
|
||||
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
|
||||
}
|
||||
|
||||
// randMonster picks a monster to show up; the lower the level, the meaner
|
||||
// the monster (monsters.c randmonster).
|
||||
func (g *RogueGame) randMonster(wander bool) byte {
|
||||
mons := &lvlMons
|
||||
if wander {
|
||||
mons = &wandMons
|
||||
}
|
||||
for {
|
||||
d := g.Depth + (g.rnd(10) - 6)
|
||||
if d < 0 {
|
||||
d = g.rnd(5)
|
||||
}
|
||||
if d > 25 {
|
||||
d = g.rnd(5) + 21
|
||||
}
|
||||
if mons[d] != 0 {
|
||||
return mons[d]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newMonster picks a new monster and adds it to the list (monsters.c
|
||||
// new_monster).
|
||||
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
|
||||
levAdd := g.Depth - AmuletLevel
|
||||
if levAdd < 0 {
|
||||
levAdd = 0
|
||||
}
|
||||
attachMon(&g.Level.Monsters, tp)
|
||||
tp.Type = typ
|
||||
tp.Disguise = typ
|
||||
tp.Pos = cp
|
||||
g.move(cp.Y, cp.X)
|
||||
tp.OldCh = g.inch()
|
||||
tp.Room = g.roomin(cp)
|
||||
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
|
||||
mp := &monsterTable[tp.Type-'A']
|
||||
tp.Stats.Lvl = mp.Stats.Lvl + levAdd
|
||||
tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8)
|
||||
tp.Stats.HP = tp.Stats.MaxHP
|
||||
tp.Stats.Arm = mp.Stats.Arm - levAdd
|
||||
tp.Stats.Dmg = mp.Stats.Dmg
|
||||
tp.Stats.Str = mp.Stats.Str
|
||||
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
|
||||
tp.Flags = mp.Flags
|
||||
if g.Depth > 29 {
|
||||
tp.Flags.Set(IsHaste)
|
||||
}
|
||||
tp.Turn = true
|
||||
tp.Pack = nil
|
||||
if g.Player.IsWearing(RAggr) {
|
||||
g.runto(cp)
|
||||
}
|
||||
if typ == 'X' {
|
||||
tp.Disguise = g.rndThing()
|
||||
}
|
||||
}
|
||||
|
||||
// expAdd is the experience to add for this monster's level/hit points
|
||||
// (monsters.c exp_add).
|
||||
func expAdd(tp *Monster) int {
|
||||
var mod int
|
||||
if tp.Stats.Lvl == 1 {
|
||||
mod = tp.Stats.MaxHP / 8
|
||||
} else {
|
||||
mod = tp.Stats.MaxHP / 6
|
||||
}
|
||||
if tp.Stats.Lvl > 9 {
|
||||
mod *= 20
|
||||
} else if tp.Stats.Lvl > 6 {
|
||||
mod *= 4
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
// wanderer creates a new wandering monster and aims it at the player
|
||||
// (monsters.c wanderer).
|
||||
func (g *RogueGame) wanderer() {
|
||||
tp := &Monster{}
|
||||
var cp Coord
|
||||
for {
|
||||
cp, _ = g.findFloor(nil, 0, true)
|
||||
if g.roomin(cp) != g.Player.Room {
|
||||
break
|
||||
}
|
||||
}
|
||||
g.newMonster(tp, g.randMonster(true), cp)
|
||||
if g.Player.On(SeeMonst) {
|
||||
g.standout()
|
||||
if !g.Player.On(IsHalu) {
|
||||
g.addch(tp.Type)
|
||||
} else {
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
}
|
||||
g.standend()
|
||||
}
|
||||
g.runto(tp.Pos)
|
||||
}
|
||||
|
||||
// wakeMonster is what to do when the hero steps next to a monster
|
||||
// (monsters.c wake_monster).
|
||||
func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
||||
p := &g.Player
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
if tp == nil {
|
||||
panic("can't find monster in wake_monster")
|
||||
}
|
||||
ch := tp.Type
|
||||
// Every time he sees a mean monster, it might start chasing him
|
||||
if !tp.On(IsRun) && g.rnd(3) != 0 && tp.On(IsMean) && !tp.On(IsHeld) &&
|
||||
!p.IsWearing(RStealth) && !p.On(IsLevit) {
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(IsRun)
|
||||
}
|
||||
if ch == 'M' && !p.On(IsBlind) && !p.On(IsHalu) &&
|
||||
!tp.On(IsFound) && !tp.On(IsCanc) && tp.On(IsRun) {
|
||||
rp := p.Room
|
||||
if (rp != nil && !rp.Flags.Has(IsDark)) ||
|
||||
distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
tp.Flags.Set(IsFound)
|
||||
if !g.save(VsMagic) {
|
||||
if p.On(IsHuh) {
|
||||
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
|
||||
} else {
|
||||
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
|
||||
}
|
||||
p.Flags.Set(IsHuh)
|
||||
mname := g.setMname(tp)
|
||||
g.addmsg("%s", mname)
|
||||
if mname != "it" {
|
||||
g.addmsg("'")
|
||||
}
|
||||
g.msg("s gaze has confused you")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Let greedy ones guard gold
|
||||
if tp.On(IsGreed) && !tp.On(IsRun) {
|
||||
tp.Flags.Set(IsRun)
|
||||
if p.Room.GoldVal != 0 {
|
||||
tp.Dest = &p.Room.Gold
|
||||
} else {
|
||||
tp.Dest = &p.Pos
|
||||
}
|
||||
}
|
||||
return tp
|
||||
}
|
||||
|
||||
// givePack gives a pack to a monster if it deserves one (monsters.c
|
||||
// give_pack).
|
||||
func (g *RogueGame) givePack(tp *Monster) {
|
||||
if g.Depth >= g.MaxDepth && g.rnd(100) < monsterTable[tp.Type-'A'].Carry {
|
||||
attachObj(&tp.Pack, g.newThing())
|
||||
}
|
||||
}
|
||||
|
||||
// saveThrow sees if a creature saves against something (monsters.c
|
||||
// save_throw).
|
||||
func (g *RogueGame) saveThrow(which int, st *Stats) bool {
|
||||
need := 14 + which - st.Lvl/2
|
||||
return g.roll(1, 20) >= need
|
||||
}
|
||||
|
||||
// save sees if the hero saves against various nasty things (monsters.c
|
||||
// save).
|
||||
func (g *RogueGame) save(which int) bool {
|
||||
p := &g.Player
|
||||
if which == VsMagic {
|
||||
if p.IsRing(Left, RProtect) {
|
||||
which -= p.CurRing[Left].Arm
|
||||
}
|
||||
if p.IsRing(Right, RProtect) {
|
||||
which -= p.CurRing[Right].Arm
|
||||
}
|
||||
}
|
||||
return g.saveThrow(which, &p.Stats)
|
||||
}
|
||||
19
game/move.go
Normal file
19
game/move.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package game
|
||||
|
||||
// move.c — hero movement. doorOpen arrives first (room entry needs it);
|
||||
// the movement commands and traps come with the UI phase.
|
||||
|
||||
// doorOpen is called to wake up things in a room that might move when the
|
||||
// hero enters (move.c door_open).
|
||||
func (g *RogueGame) doorOpen(rp *Room) {
|
||||
if rp.Flags.Has(IsGone) {
|
||||
return
|
||||
}
|
||||
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ {
|
||||
for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
|
||||
if isUpper(g.Level.VisibleChar(y, x)) {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
game/newlevel.go
Normal file
159
game/newlevel.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package game
|
||||
|
||||
// new_level.c — dig and draw a new level.
|
||||
|
||||
const (
|
||||
treasRoomChance = 20 // one chance in TREAS_ROOM for a treasure room
|
||||
maxTreas = 10 // maximum number of treasures in a treasure room
|
||||
minTreas = 2 // minimum number of treasures in a treasure room
|
||||
maxTries = 10 // max number of tries to put down a monster
|
||||
)
|
||||
|
||||
// NewLevel digs and draws a new level (new_level.c new_level).
|
||||
func (g *RogueGame) NewLevel() {
|
||||
p := &g.Player
|
||||
p.Flags.Clear(IsHeld) // unhold when you go down just in case
|
||||
if g.Depth > g.MaxDepth {
|
||||
g.MaxDepth = g.Depth
|
||||
}
|
||||
// Clean things off from last level
|
||||
for i := range g.Level.Places {
|
||||
g.Level.Places[i] = Place{Ch: ' ', Flags: FReal}
|
||||
}
|
||||
g.clear()
|
||||
// Free up the monsters on the last level; the objects and their packs
|
||||
// go with them (the garbage collector is our free_list).
|
||||
g.Level.Monsters = nil
|
||||
g.Level.Objects = nil
|
||||
g.doRooms() // Draw rooms
|
||||
g.doPassages() // Draw passages
|
||||
p.NoFood++
|
||||
g.putThings() // Place objects (if any)
|
||||
// Place the traps
|
||||
if g.rnd(10) < g.Depth {
|
||||
g.Level.NTraps = g.rnd(g.Depth/4) + 1
|
||||
if g.Level.NTraps > MaxTraps {
|
||||
g.Level.NTraps = MaxTraps
|
||||
}
|
||||
for i := g.Level.NTraps; i > 0; i-- {
|
||||
// not only wouldn't it be NICE to have traps in mazes (not
|
||||
// that we care about being nice), since the trap number is
|
||||
// stored where the passage number is, we can't actually do it.
|
||||
var stairs Coord
|
||||
for {
|
||||
stairs, _ = g.findFloor(nil, 0, false)
|
||||
if g.Level.Char(stairs.Y, stairs.X) == Floor {
|
||||
break
|
||||
}
|
||||
}
|
||||
sp := g.Level.FlagsAt(stairs.Y, stairs.X)
|
||||
sp.Clear(FReal)
|
||||
*sp |= PlaceFlags(g.rnd(NTraps))
|
||||
}
|
||||
}
|
||||
// Place the staircase down.
|
||||
stairs, _ := g.findFloor(nil, 0, false)
|
||||
g.Level.Stairs = stairs
|
||||
g.Level.SetChar(stairs.Y, stairs.X, Stairs)
|
||||
g.SeenStairs = false
|
||||
|
||||
for _, tp := range g.Level.Monsters {
|
||||
tp.Room = g.roomin(tp.Pos)
|
||||
}
|
||||
|
||||
hero, _ := g.findFloor(nil, 0, true)
|
||||
p.Pos = hero
|
||||
g.enterRoom(hero)
|
||||
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
||||
if p.On(SeeMonst) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
if p.On(IsHalu) {
|
||||
g.visuals(0)
|
||||
}
|
||||
}
|
||||
|
||||
// rndRoom picks a room that is really there (new_level.c rnd_room).
|
||||
func (g *RogueGame) rndRoom() int {
|
||||
for {
|
||||
rm := g.rnd(MaxRooms)
|
||||
if !g.Level.Rooms[rm].Flags.Has(IsGone) {
|
||||
return rm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putThings puts potions and scrolls on this level (new_level.c
|
||||
// put_things).
|
||||
func (g *RogueGame) putThings() {
|
||||
// Once you have found the amulet, the only way to get new stuff is to
|
||||
// go down into the dungeon.
|
||||
if g.HasAmulet && g.Depth < g.MaxDepth {
|
||||
return
|
||||
}
|
||||
// check for treasure rooms, and if so, put it in.
|
||||
if g.rnd(treasRoomChance) == 0 {
|
||||
g.treasRoom()
|
||||
}
|
||||
// Do MAXOBJ attempts to put things on a level
|
||||
for i := 0; i < MaxObj; i++ {
|
||||
if g.rnd(100) < 36 {
|
||||
// Pick a new object and link it in the list
|
||||
obj := g.newThing()
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
// Put it somewhere
|
||||
obj.Pos, _ = g.findFloor(nil, 0, false)
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Type)
|
||||
}
|
||||
}
|
||||
// If he is really deep in the dungeon and he hasn't found the amulet
|
||||
// yet, put it somewhere on the ground
|
||||
if g.Depth >= AmuletLevel && !g.HasAmulet {
|
||||
obj := newObject()
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
obj.Damage = "0x0"
|
||||
obj.HurlDmg = "0x0"
|
||||
obj.Arm = 11
|
||||
obj.Type = Amulet
|
||||
// Put it somewhere
|
||||
obj.Pos, _ = g.findFloor(nil, 0, false)
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Amulet)
|
||||
}
|
||||
}
|
||||
|
||||
// treasRoom adds a treasure room (new_level.c treas_room).
|
||||
func (g *RogueGame) treasRoom() {
|
||||
rp := &g.Level.Rooms[g.rndRoom()]
|
||||
spots := (rp.Max.Y-2)*(rp.Max.X-2) - minTreas
|
||||
if spots > maxTreas-minTreas {
|
||||
spots = maxTreas - minTreas
|
||||
}
|
||||
numMonst := g.rnd(spots) + minTreas
|
||||
for nm := numMonst; nm > 0; nm-- {
|
||||
mp, _ := g.findFloorIn(rp, 2*maxTries, false)
|
||||
tp := g.newThing()
|
||||
tp.Pos = mp
|
||||
attachObj(&g.Level.Objects, tp)
|
||||
g.Level.SetChar(mp.Y, mp.X, tp.Type)
|
||||
}
|
||||
|
||||
// fill up room with monsters from the next level down
|
||||
nm := g.rnd(spots) + minTreas
|
||||
if nm < numMonst+2 {
|
||||
nm = numMonst + 2
|
||||
}
|
||||
spots = (rp.Max.Y - 2) * (rp.Max.X - 2)
|
||||
if nm > spots {
|
||||
nm = spots
|
||||
}
|
||||
g.Depth++
|
||||
for ; nm > 0; nm-- {
|
||||
if mp, ok := g.findFloorIn(rp, maxTries, true); ok {
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
tp.Flags.Set(IsMean) // no sloughers in THIS room
|
||||
g.givePack(tp)
|
||||
}
|
||||
}
|
||||
g.Depth--
|
||||
}
|
||||
124
game/newlevel_test.go
Normal file
124
game/newlevel_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func genLevel(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
g := NewGame(Config{Seed: seed})
|
||||
g.NewLevel()
|
||||
return g
|
||||
}
|
||||
|
||||
// renderMap draws the raw level map (not the screen) as text.
|
||||
func renderMap(g *RogueGame) string {
|
||||
var sb strings.Builder
|
||||
for y := 0; y < NumLines; y++ {
|
||||
for x := 0; x < NumCols; x++ {
|
||||
ch := g.Level.Char(y, x)
|
||||
if m := g.Level.MonsterAt(y, x); m != nil {
|
||||
ch = m.Type
|
||||
}
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestNewLevelInvariants(t *testing.T) {
|
||||
for _, seed := range []int32{1, 12345, 2026, 99999} {
|
||||
g := genLevel(t, seed)
|
||||
|
||||
// The staircase is somewhere real.
|
||||
st := g.Level.Stairs
|
||||
if g.Level.Char(st.Y, st.X) != Stairs {
|
||||
t.Errorf("seed %d: no staircase at recorded stairs position", seed)
|
||||
}
|
||||
|
||||
// The hero stands on a walkable, monster-free cell.
|
||||
hp := g.Player.Pos
|
||||
if !stepOk(g.Level.Char(hp.Y, hp.X)) {
|
||||
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
|
||||
g.Level.Char(hp.Y, hp.X))
|
||||
}
|
||||
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
|
||||
t.Errorf("seed %d: hero standing on a monster", seed)
|
||||
}
|
||||
if g.Player.Room == nil {
|
||||
t.Errorf("seed %d: hero not in any room", seed)
|
||||
}
|
||||
|
||||
// Some rooms exist and are drawn.
|
||||
m := renderMap(g)
|
||||
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
|
||||
t.Errorf("seed %d: no room walls drawn", seed)
|
||||
}
|
||||
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
|
||||
t.Errorf("seed %d: no floor or passages drawn", seed)
|
||||
}
|
||||
|
||||
// Every monster is indexed on the map and placed in a room.
|
||||
for _, mon := range g.Level.Monsters {
|
||||
if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon {
|
||||
t.Errorf("seed %d: monster %c not indexed at its position",
|
||||
seed, mon.Type)
|
||||
}
|
||||
if mon.Room == nil {
|
||||
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Every level object sits on a cell displaying its type (items can
|
||||
// share cells only with monsters standing on them).
|
||||
for _, obj := range g.Level.Objects {
|
||||
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
|
||||
if ch != obj.Type && g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil {
|
||||
t.Errorf("seed %d: object %q at (%d,%d) but map shows %q",
|
||||
seed, obj.Type, obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
}
|
||||
|
||||
// The player has her starting kit: food, armor, mace, bow, arrows.
|
||||
if len(g.Player.Pack) != 5 {
|
||||
t.Errorf("seed %d: starting pack has %d items, want 5",
|
||||
seed, len(g.Player.Pack))
|
||||
}
|
||||
if g.Player.CurWeapon == nil || g.Player.CurWeapon.Which != Mace {
|
||||
t.Errorf("seed %d: not wielding the starting mace", seed)
|
||||
}
|
||||
if g.Player.CurArmor == nil || g.Player.CurArmor.Which != RingMail {
|
||||
t.Errorf("seed %d: not wearing the starting ring mail", seed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLevelDeterministic(t *testing.T) {
|
||||
a := renderMap(genLevel(t, 12345))
|
||||
b := renderMap(genLevel(t, 12345))
|
||||
if a != b {
|
||||
t.Error("same seed produced different levels")
|
||||
}
|
||||
c := renderMap(genLevel(t, 54321))
|
||||
if a == c {
|
||||
t.Error("different seeds produced identical levels")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeeperLevels exercises generation across many depths and seeds —
|
||||
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
|
||||
func TestDeeperLevels(t *testing.T) {
|
||||
for _, seed := range []int32{7, 42, 1000, 31337} {
|
||||
g := NewGame(Config{Seed: seed})
|
||||
for depth := 1; depth <= 30; depth++ {
|
||||
g.Depth = depth
|
||||
g.NewLevel()
|
||||
st := g.Level.Stairs
|
||||
if g.Level.Char(st.Y, st.X) != Stairs {
|
||||
t.Fatalf("seed %d depth %d: missing staircase", seed, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
400
game/pack.go
Normal file
400
game/pack.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package game
|
||||
|
||||
// pack.c — routines to deal with the pack.
|
||||
|
||||
// addPack picks up an object and adds it to the pack; if obj is non-nil use
|
||||
// it instead of getting it off the ground (pack.c add_pack).
|
||||
func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
p := &g.Player
|
||||
fromFloor := false
|
||||
if obj == nil {
|
||||
if obj = g.findObj(p.Pos.Y, p.Pos.X); obj == nil {
|
||||
return
|
||||
}
|
||||
fromFloor = true
|
||||
}
|
||||
|
||||
// Check for and deal with scare monster scrolls
|
||||
if obj.Type == Scroll && obj.Which == SScare && obj.Flags.Has(ObjIsFound) {
|
||||
detachObj(&g.Level.Objects, obj)
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
||||
if p.Room.Flags.Has(IsGone) {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
|
||||
}
|
||||
g.msg("the scroll turns to dust as you pick it up")
|
||||
return
|
||||
}
|
||||
|
||||
if len(p.Pack) == 0 {
|
||||
p.Pack = append(p.Pack, obj)
|
||||
obj.PackCh = g.packChar()
|
||||
p.Inpack++
|
||||
} else {
|
||||
// Walk the pack looking for the insertion point, keeping items of
|
||||
// one type together and merging stackable/grouped items — a direct
|
||||
// translation of the C linked-list walk. lp is the index to insert
|
||||
// after; -1 after a merge means no insertion.
|
||||
lp := -1
|
||||
merged := false
|
||||
for i := 0; i < len(p.Pack); i++ {
|
||||
if p.Pack[i].Type != obj.Type {
|
||||
lp = i
|
||||
continue
|
||||
}
|
||||
// found the group of our type: scan for matching subtype
|
||||
for p.Pack[i].Type == obj.Type && p.Pack[i].Which != obj.Which {
|
||||
lp = i
|
||||
if i+1 >= len(p.Pack) {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
op := p.Pack[i]
|
||||
if op.Type == obj.Type && op.Which == obj.Which {
|
||||
if IsMult(op.Type) {
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return
|
||||
}
|
||||
op.Count++
|
||||
obj = op
|
||||
lp = -1
|
||||
merged = true
|
||||
} else if obj.Group != 0 {
|
||||
lp = i
|
||||
for p.Pack[i].Type == obj.Type &&
|
||||
p.Pack[i].Which == obj.Which &&
|
||||
p.Pack[i].Group != obj.Group {
|
||||
lp = i
|
||||
if i+1 >= len(p.Pack) {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
op = p.Pack[i]
|
||||
if op.Type == obj.Type && op.Which == obj.Which &&
|
||||
op.Group == obj.Group {
|
||||
op.Count += obj.Count
|
||||
p.Inpack--
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return
|
||||
}
|
||||
obj = op
|
||||
lp = -1
|
||||
merged = true
|
||||
}
|
||||
} else {
|
||||
lp = i
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if !merged && lp != -1 {
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return
|
||||
}
|
||||
obj.PackCh = g.packChar()
|
||||
p.Pack = append(p.Pack[:lp+1],
|
||||
append([]*Object{obj}, p.Pack[lp+1:]...)...)
|
||||
}
|
||||
}
|
||||
|
||||
obj.Flags.Set(ObjIsFound)
|
||||
|
||||
// If this was the object of something's desire, that monster will get
|
||||
// mad and run at the hero.
|
||||
for _, op := range g.Level.Monsters {
|
||||
if op.Dest == &obj.Pos {
|
||||
op.Dest = &p.Pos
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Type == Amulet {
|
||||
g.HasAmulet = true
|
||||
}
|
||||
// Notify the user
|
||||
if !silent {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you now have ")
|
||||
}
|
||||
g.msg("%s (%c)", g.invName(obj, !g.Options.Terse), obj.PackCh)
|
||||
}
|
||||
}
|
||||
|
||||
// packRoom sees if there's room in the pack; if not, prints an appropriate
|
||||
// message (pack.c pack_room).
|
||||
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
|
||||
p := &g.Player
|
||||
if p.Inpack++; p.Inpack > MaxPack {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("there's ")
|
||||
}
|
||||
g.addmsg("no room")
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" in your pack")
|
||||
}
|
||||
g.endmsg()
|
||||
if fromFloor {
|
||||
g.moveMsg(obj)
|
||||
}
|
||||
p.Inpack = MaxPack
|
||||
return false
|
||||
}
|
||||
|
||||
if fromFloor {
|
||||
detachObj(&g.Level.Objects, obj)
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
||||
if p.Room.Flags.Has(IsGone) {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// leavePack takes an item out of the pack (pack.c leave_pack).
|
||||
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
|
||||
p := &g.Player
|
||||
p.Inpack--
|
||||
nobj := obj
|
||||
if obj.Count > 1 && !all {
|
||||
g.LastPick = obj
|
||||
obj.Count--
|
||||
if obj.Group != 0 {
|
||||
p.Inpack++
|
||||
}
|
||||
if newobj {
|
||||
copied := *obj
|
||||
nobj = &copied
|
||||
nobj.Count = 1
|
||||
}
|
||||
} else {
|
||||
g.LastPick = nil
|
||||
p.PackUsed[obj.PackCh-'a'] = false
|
||||
detachObj(&p.Pack, obj)
|
||||
}
|
||||
return nobj
|
||||
}
|
||||
|
||||
// packChar returns the next unused pack character (pack.c pack_char).
|
||||
func (g *RogueGame) packChar() byte {
|
||||
p := &g.Player
|
||||
for i := range p.PackUsed {
|
||||
if !p.PackUsed[i] {
|
||||
p.PackUsed[i] = true
|
||||
return byte(i) + 'a'
|
||||
}
|
||||
}
|
||||
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
|
||||
}
|
||||
|
||||
// inventory lists what is in the pack; returns true if there is something
|
||||
// of the given type (pack.c inventory).
|
||||
func (g *RogueGame) inventory(list []*Object, typ int) bool {
|
||||
g.NObjs = 0
|
||||
for _, item := range list {
|
||||
if typ != 0 && typ != int(item.Type) &&
|
||||
!(typ == Callable && item.Type != Food && item.Type != Amulet) &&
|
||||
!(typ == RorS && (item.Type == Ring || item.Type == Stick)) {
|
||||
continue
|
||||
}
|
||||
g.NObjs++
|
||||
g.Msgs.MsgEsc = true
|
||||
line := string(item.PackCh) + ") " + g.invName(item, false)
|
||||
if g.addLine("%s", line) == Escape {
|
||||
g.Msgs.MsgEsc = false
|
||||
g.msg("")
|
||||
return true
|
||||
}
|
||||
g.Msgs.MsgEsc = false
|
||||
}
|
||||
if g.NObjs == 0 {
|
||||
if g.Options.Terse {
|
||||
if typ == 0 {
|
||||
g.msg("empty handed")
|
||||
} else {
|
||||
g.msg("nothing appropriate")
|
||||
}
|
||||
} else {
|
||||
if typ == 0 {
|
||||
g.msg("you are empty handed")
|
||||
} else {
|
||||
g.msg("you don't have anything appropriate")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
g.endLine()
|
||||
return true
|
||||
}
|
||||
|
||||
// pickUp adds something to the character's pack (pack.c pick_up).
|
||||
func (g *RogueGame) pickUp(ch byte) {
|
||||
p := &g.Player
|
||||
if p.On(IsLevit) {
|
||||
return
|
||||
}
|
||||
|
||||
obj := g.findObj(p.Pos.Y, p.Pos.X)
|
||||
if g.MoveOn {
|
||||
g.moveMsg(obj)
|
||||
return
|
||||
}
|
||||
switch ch {
|
||||
case Gold:
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
g.money(obj.GoldVal())
|
||||
detachObj(&g.Level.Objects, obj)
|
||||
p.Room.GoldVal = 0
|
||||
default:
|
||||
g.addPack(nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
// moveMsg prints the message if you are just moving onto an object
|
||||
// (pack.c move_msg).
|
||||
func (g *RogueGame) moveMsg(obj *Object) {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you ")
|
||||
}
|
||||
g.msg("moved onto %s", g.invName(obj, true))
|
||||
}
|
||||
|
||||
// pickyInven allows the player to inventory a single item (pack.c
|
||||
// picky_inven).
|
||||
func (g *RogueGame) pickyInven() {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you aren't carrying anything")
|
||||
return
|
||||
}
|
||||
if len(p.Pack) == 1 {
|
||||
g.msg("a) %s", g.invName(p.Pack[0], false))
|
||||
return
|
||||
}
|
||||
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
|
||||
g.Msgs.Mpos = 0
|
||||
mch := g.readchar()
|
||||
if mch == Escape {
|
||||
g.msg("")
|
||||
return
|
||||
}
|
||||
for _, obj := range p.Pack {
|
||||
if mch == obj.PackCh {
|
||||
g.msg("%c) %s", mch, g.invName(obj, false))
|
||||
return
|
||||
}
|
||||
}
|
||||
g.msg("'%s' not in pack", unctrl(mch))
|
||||
}
|
||||
|
||||
// getItem picks something out of a pack for a purpose (pack.c get_item).
|
||||
func (g *RogueGame) getItem(purpose string, typ int) *Object {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you aren't carrying anything")
|
||||
return nil
|
||||
}
|
||||
if g.Again {
|
||||
if g.LastPick != nil {
|
||||
return g.LastPick
|
||||
}
|
||||
g.msg("you ran out")
|
||||
return nil
|
||||
}
|
||||
for {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("which object do you want to ")
|
||||
}
|
||||
g.addmsg("%s", purpose)
|
||||
if g.Options.Terse {
|
||||
g.addmsg(" what")
|
||||
}
|
||||
g.msg("? (* for list): ")
|
||||
ch := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
// Give the poor player a chance to abort the command
|
||||
if ch == Escape {
|
||||
g.resetLast()
|
||||
g.After = false
|
||||
g.msg("")
|
||||
return nil
|
||||
}
|
||||
g.NObjs = 1 // normal case: person types one char
|
||||
if ch == '*' {
|
||||
g.Msgs.Mpos = 0
|
||||
if !g.inventory(p.Pack, typ) {
|
||||
g.After = false
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, obj := range p.Pack {
|
||||
if obj.PackCh == ch {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
g.msg("'%s' is not a valid item", unctrl(ch))
|
||||
}
|
||||
}
|
||||
|
||||
// money adds or subtracts gold from the pack (pack.c money).
|
||||
func (g *RogueGame) money(value int) {
|
||||
p := &g.Player
|
||||
p.Purse += value
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
||||
if p.Room.Flags.Has(IsGone) {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
|
||||
}
|
||||
if value > 0 {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you found ")
|
||||
}
|
||||
g.msg("%d gold pieces", value)
|
||||
}
|
||||
}
|
||||
|
||||
// floorCh returns the appropriate floor character for her room
|
||||
// (pack.c floor_ch).
|
||||
func (g *RogueGame) floorCh() byte {
|
||||
if g.Player.Room.Flags.Has(IsGone) {
|
||||
return Passage
|
||||
}
|
||||
if g.showFloor() {
|
||||
return Floor
|
||||
}
|
||||
return ' '
|
||||
}
|
||||
|
||||
// floorAt returns the character at the hero's position, taking see_floor
|
||||
// into account (pack.c floor_at).
|
||||
func (g *RogueGame) floorAt() byte {
|
||||
ch := g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X)
|
||||
if ch == Floor {
|
||||
ch = g.floorCh()
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// resetLast resets the last command when the current one is aborted
|
||||
// (pack.c reset_last).
|
||||
func (g *RogueGame) resetLast() {
|
||||
g.LastComm = g.LLastComm
|
||||
g.LastDir = g.LLastDir
|
||||
g.LastPick = g.LLastPick
|
||||
}
|
||||
|
||||
// chooseTerse picks the terse or verbose variant of a message.
|
||||
func (g *RogueGame) chooseTerse(terse, verbose string) string {
|
||||
if g.Options.Terse {
|
||||
return terse
|
||||
}
|
||||
return verbose
|
||||
}
|
||||
302
game/passages.go
Normal file
302
game/passages.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package game
|
||||
|
||||
// passages.c — draw the connecting passages.
|
||||
|
||||
// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages.
|
||||
var rdesConn = [MaxRooms][MaxRooms]bool{
|
||||
{false, true, false, true, false, false, false, false, false},
|
||||
{true, false, true, false, true, false, false, false, false},
|
||||
{false, true, false, false, false, true, false, false, false},
|
||||
{true, false, false, false, true, false, true, false, false},
|
||||
{false, true, false, true, false, true, false, true, false},
|
||||
{false, false, true, false, true, false, false, false, true},
|
||||
{false, false, false, true, false, false, false, true, false},
|
||||
{false, false, false, false, true, false, true, false, true},
|
||||
{false, false, false, false, false, true, false, true, false},
|
||||
}
|
||||
|
||||
// doPassages draws all the passages on a level (passages.c do_passages).
|
||||
func (g *RogueGame) doPassages() {
|
||||
var isconn [MaxRooms][MaxRooms]bool
|
||||
var ingraph [MaxRooms]bool
|
||||
|
||||
// starting with one room, connect it to a random adjacent room and
|
||||
// then pick a new room to start with.
|
||||
roomcount := 1
|
||||
r1 := g.rnd(MaxRooms)
|
||||
ingraph[r1] = true
|
||||
for {
|
||||
// find a room to connect with
|
||||
j := 0
|
||||
r2 := -1
|
||||
for i := 0; i < MaxRooms; i++ {
|
||||
if rdesConn[r1][i] && !ingraph[i] {
|
||||
if j++; g.rnd(j) == 0 {
|
||||
r2 = i
|
||||
}
|
||||
}
|
||||
}
|
||||
if j == 0 {
|
||||
// if no adjacent rooms are outside the graph, pick a new room
|
||||
// to look from
|
||||
for {
|
||||
r1 = g.rnd(MaxRooms)
|
||||
if ingraph[r1] {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// otherwise, connect new room to the graph, and draw a tunnel
|
||||
// to it
|
||||
ingraph[r2] = true
|
||||
g.conn(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
roomcount++
|
||||
}
|
||||
if roomcount >= MaxRooms {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to add passages to the graph a random number of times so that
|
||||
// there isn't always just one unique passage through it.
|
||||
for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
|
||||
r1 = g.rnd(MaxRooms) // a random room to look from
|
||||
// find an adjacent room not already connected
|
||||
j := 0
|
||||
r2 := -1
|
||||
for i := 0; i < MaxRooms; i++ {
|
||||
if rdesConn[r1][i] && !isconn[r1][i] {
|
||||
if j++; g.rnd(j) == 0 {
|
||||
r2 = i
|
||||
}
|
||||
}
|
||||
}
|
||||
// if there is one, connect it and look for the next added passage
|
||||
if j != 0 {
|
||||
g.conn(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
}
|
||||
}
|
||||
g.passnum()
|
||||
}
|
||||
|
||||
// conn draws a corridor from a room in a certain direction (passages.c
|
||||
// conn).
|
||||
func (g *RogueGame) conn(r1, r2 int) {
|
||||
var rm int
|
||||
var direc byte
|
||||
if r1 < r2 {
|
||||
rm = r1
|
||||
if r1+1 == r2 {
|
||||
direc = 'r'
|
||||
} else {
|
||||
direc = 'd'
|
||||
}
|
||||
} else {
|
||||
rm = r2
|
||||
if r2+1 == r1 {
|
||||
direc = 'r'
|
||||
} else {
|
||||
direc = 'd'
|
||||
}
|
||||
}
|
||||
rpf := &g.Level.Rooms[rm]
|
||||
|
||||
// Set up the movement variables, in two cases: first drawing one down.
|
||||
var rpt *Room
|
||||
var del, turnDelta, spos, epos Coord
|
||||
var distance, turnDistance int
|
||||
if direc == 'd' {
|
||||
rmt := rm + 3 // room # of dest
|
||||
rpt = &g.Level.Rooms[rmt] // room pointer of dest
|
||||
del = Coord{X: 0, Y: 1} // direction of move
|
||||
spos = rpf.Pos // start of move
|
||||
epos = rpt.Pos // end of move
|
||||
if !rpf.Flags.Has(IsGone) { // if not gone pick door pos
|
||||
for {
|
||||
spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
|
||||
spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
|
||||
if !(rpf.Flags.Has(IsMaze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPass)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !rpt.Flags.Has(IsGone) {
|
||||
for {
|
||||
epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
|
||||
if !(rpt.Flags.Has(IsMaze) && !g.Level.FlagsAt(epos.Y, epos.X).Has(FPass)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
distance = abs(spos.Y-epos.Y) - 1 // distance to move
|
||||
turnDelta.Y = 0 // direction to turn
|
||||
if spos.X < epos.X {
|
||||
turnDelta.X = 1
|
||||
} else {
|
||||
turnDelta.X = -1
|
||||
}
|
||||
turnDistance = abs(spos.X - epos.X) // how far to turn
|
||||
} else { // setup for moving right
|
||||
rmt := rm + 1
|
||||
rpt = &g.Level.Rooms[rmt]
|
||||
del = Coord{X: 1, Y: 0}
|
||||
spos = rpf.Pos
|
||||
epos = rpt.Pos
|
||||
if !rpf.Flags.Has(IsGone) {
|
||||
for {
|
||||
spos.X = rpf.Pos.X + rpf.Max.X - 1
|
||||
spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
|
||||
if !(rpf.Flags.Has(IsMaze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPass)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !rpt.Flags.Has(IsGone) {
|
||||
for {
|
||||
epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
|
||||
if !(rpt.Flags.Has(IsMaze) && !g.Level.FlagsAt(epos.Y, epos.X).Has(FPass)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
distance = abs(spos.X-epos.X) - 1
|
||||
if spos.Y < epos.Y {
|
||||
turnDelta.Y = 1
|
||||
} else {
|
||||
turnDelta.Y = -1
|
||||
}
|
||||
turnDelta.X = 0
|
||||
turnDistance = abs(spos.Y - epos.Y)
|
||||
}
|
||||
|
||||
turnSpot := g.rnd(distance-1) + 1 // where turn starts
|
||||
|
||||
// Draw in the doors on either side of the passage or just put #'s if
|
||||
// the rooms are gone.
|
||||
if !rpf.Flags.Has(IsGone) {
|
||||
g.door(rpf, spos)
|
||||
} else {
|
||||
g.putpass(spos)
|
||||
}
|
||||
if !rpt.Flags.Has(IsGone) {
|
||||
g.door(rpt, epos)
|
||||
} else {
|
||||
g.putpass(epos)
|
||||
}
|
||||
// Get ready to move...
|
||||
curr := spos
|
||||
for distance > 0 {
|
||||
// Move to new position
|
||||
curr.X += del.X
|
||||
curr.Y += del.Y
|
||||
// Check if we are at the turn place, if so do the turn
|
||||
if distance == turnSpot {
|
||||
for ; turnDistance > 0; turnDistance-- {
|
||||
g.putpass(curr)
|
||||
curr.X += turnDelta.X
|
||||
curr.Y += turnDelta.Y
|
||||
}
|
||||
}
|
||||
// Continue digging along
|
||||
g.putpass(curr)
|
||||
distance--
|
||||
}
|
||||
curr.X += del.X
|
||||
curr.Y += del.Y
|
||||
if curr != epos {
|
||||
g.msg("warning, connectivity problem on this level")
|
||||
}
|
||||
}
|
||||
|
||||
// putpass adds a passage character or secret passage here (passages.c
|
||||
// putpass).
|
||||
func (g *RogueGame) putpass(cp Coord) {
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
pp.Flags.Set(FPass)
|
||||
if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 {
|
||||
pp.Flags.Clear(FReal)
|
||||
} else {
|
||||
pp.Ch = Passage
|
||||
}
|
||||
}
|
||||
|
||||
// door adds a door or possibly a secret door; also enters the door in the
|
||||
// exits array of the room (passages.c door).
|
||||
func (g *RogueGame) door(rm *Room, cp Coord) {
|
||||
rm.Exits = append(rm.Exits, cp)
|
||||
|
||||
if rm.Flags.Has(IsMaze) {
|
||||
return
|
||||
}
|
||||
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
if g.rnd(10)+1 < g.Depth && g.rnd(5) == 0 {
|
||||
if cp.Y == rm.Pos.Y || cp.Y == rm.Pos.Y+rm.Max.Y-1 {
|
||||
pp.Ch = '-'
|
||||
} else {
|
||||
pp.Ch = '|'
|
||||
}
|
||||
pp.Flags.Clear(FReal)
|
||||
} else {
|
||||
pp.Ch = Door
|
||||
}
|
||||
}
|
||||
|
||||
// passnum assigns a number to each passageway (passages.c passnum).
|
||||
func (g *RogueGame) passnum() {
|
||||
g.pnum = 0
|
||||
g.newpnum = false
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0]
|
||||
}
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
for j := range rp.Exits {
|
||||
g.newpnum = true
|
||||
g.numpass(rp.Exits[j].Y, rp.Exits[j].X)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// numpass numbers a passageway square and its brethren (passages.c
|
||||
// numpass).
|
||||
func (g *RogueGame) numpass(y, x int) {
|
||||
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
|
||||
return
|
||||
}
|
||||
fp := g.Level.FlagsAt(y, x)
|
||||
if fp.Has(FPNum) {
|
||||
return
|
||||
}
|
||||
if g.newpnum {
|
||||
g.pnum++
|
||||
g.newpnum = false
|
||||
}
|
||||
// check to see if it is a door or secret door, i.e., a new exit, or a
|
||||
// numerable type of place
|
||||
if ch := g.Level.Char(y, x); ch == Door ||
|
||||
(!fp.Has(FReal) && (ch == '|' || ch == '-')) {
|
||||
rp := &g.Level.Passages[g.pnum]
|
||||
rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
|
||||
} else if !fp.Has(FPass) {
|
||||
return
|
||||
}
|
||||
*fp |= PlaceFlags(g.pnum)
|
||||
// recurse on the surrounding places
|
||||
g.numpass(y+1, x)
|
||||
g.numpass(y-1, x)
|
||||
g.numpass(y, x+1)
|
||||
g.numpass(y, x-1)
|
||||
}
|
||||
|
||||
// abs is C abs() for ints.
|
||||
func abs(n int) int {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
85
game/potions.go
Normal file
85
game/potions.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package game
|
||||
|
||||
// potions.c — the visibility-related helpers arrive first; quaff and the
|
||||
// potion effect dispatch come with the effects phase.
|
||||
|
||||
// isMagic reports whether an object radiates magic (potions.c is_magic).
|
||||
func (o *Object) isMagic() bool {
|
||||
switch o.Type {
|
||||
case Armor:
|
||||
return o.Flags.Has(IsProt) || o.Arm != aClass[o.Which]
|
||||
case Weapon:
|
||||
return o.HPlus != 0 || o.DPlus != 0
|
||||
case Potion, Scroll, Stick, Ring, Amulet:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// invisOn turns on the ability to see invisible monsters (potions.c
|
||||
// invis_on).
|
||||
func (g *RogueGame) invisOn() {
|
||||
g.Player.Flags.Set(CanSee)
|
||||
for _, mp := range g.Level.Monsters {
|
||||
if mp.On(IsInvis) && g.seeMonst(mp) && !g.Player.On(IsHalu) {
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// turnSee puts on or off seeing monsters on this level (potions.c
|
||||
// turn_see).
|
||||
func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
addNew := false
|
||||
for _, mp := range g.Level.Monsters {
|
||||
g.move(mp.Pos.Y, mp.Pos.X)
|
||||
canSee := g.seeMonst(mp)
|
||||
if turnOff {
|
||||
if !canSee {
|
||||
g.addch(mp.OldCh)
|
||||
}
|
||||
} else {
|
||||
if !canSee {
|
||||
g.standout()
|
||||
}
|
||||
if !g.Player.On(IsHalu) {
|
||||
g.addch(mp.Type)
|
||||
} else {
|
||||
g.addch(byte(g.rnd(26) + 'A'))
|
||||
}
|
||||
if !canSee {
|
||||
g.standend()
|
||||
addNew = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if turnOff {
|
||||
g.Player.Flags.Clear(SeeMonst)
|
||||
} else {
|
||||
g.Player.Flags.Set(SeeMonst)
|
||||
}
|
||||
return addNew
|
||||
}
|
||||
|
||||
// seenStairs reports whether the player has seen the stairs (potions.c
|
||||
// seen_stairs).
|
||||
func (g *RogueGame) seenStairs() bool {
|
||||
st := g.Level.Stairs
|
||||
g.move(st.Y, st.X)
|
||||
if g.inch() == Stairs { // it's on the map
|
||||
return true
|
||||
}
|
||||
if g.Player.Pos == st { // it's under him
|
||||
return true
|
||||
}
|
||||
// if a monster is on the stairs, this gets hairy
|
||||
if tp := g.Level.MonsterAt(st.Y, st.X); tp != nil {
|
||||
if g.seeMonst(tp) && tp.On(IsRun) { // visible and awake:
|
||||
return true // it must have moved there
|
||||
}
|
||||
if g.Player.On(SeeMonst) && tp.OldCh == Stairs {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
174
game/rings.go
Normal file
174
game/rings.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// rings.c — routines dealing specifically with rings.
|
||||
|
||||
// ringOn puts a ring on a hand (rings.c ring_on).
|
||||
func (g *RogueGame) ringOn() {
|
||||
p := &g.Player
|
||||
obj := g.getItem("put on", int(Ring))
|
||||
// Make certain that it is something that we want to wear
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if obj.Type != Ring {
|
||||
if !g.Options.Terse {
|
||||
g.msg("it would be difficult to wrap that around a finger")
|
||||
} else {
|
||||
g.msg("not a ring")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// find out which hand to put it on
|
||||
if g.isCurrent(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
var ring int
|
||||
switch {
|
||||
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
|
||||
if ring = g.gethand(); ring < 0 {
|
||||
return
|
||||
}
|
||||
case p.CurRing[Left] == nil:
|
||||
ring = Left
|
||||
case p.CurRing[Right] == nil:
|
||||
ring = Right
|
||||
default:
|
||||
if !g.Options.Terse {
|
||||
g.msg("you already have a ring on each hand")
|
||||
} else {
|
||||
g.msg("wearing two")
|
||||
}
|
||||
return
|
||||
}
|
||||
p.CurRing[ring] = obj
|
||||
|
||||
// Calculate the effect it has on the poor guy.
|
||||
switch obj.Which {
|
||||
case RAddStr:
|
||||
g.chgStr(obj.Arm)
|
||||
case RSeeInvis:
|
||||
g.invisOn()
|
||||
case RAggr:
|
||||
g.aggravate()
|
||||
}
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are now wearing ")
|
||||
}
|
||||
g.msg("%s (%c)", g.invName(obj, true), obj.PackCh)
|
||||
}
|
||||
|
||||
// ringOff takes off a ring (rings.c ring_off).
|
||||
func (g *RogueGame) ringOff() {
|
||||
p := &g.Player
|
||||
var ring int
|
||||
switch {
|
||||
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
|
||||
if g.Options.Terse {
|
||||
g.msg("no rings")
|
||||
} else {
|
||||
g.msg("you aren't wearing any rings")
|
||||
}
|
||||
return
|
||||
case p.CurRing[Left] == nil:
|
||||
ring = Right
|
||||
case p.CurRing[Right] == nil:
|
||||
ring = Left
|
||||
default:
|
||||
if ring = g.gethand(); ring < 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
g.Msgs.Mpos = 0
|
||||
obj := p.CurRing[ring]
|
||||
if obj == nil {
|
||||
g.msg("not wearing such a ring")
|
||||
return
|
||||
}
|
||||
if g.dropCheck(obj) {
|
||||
g.msg("was wearing %s(%c)", g.invName(obj, true), obj.PackCh)
|
||||
}
|
||||
}
|
||||
|
||||
// gethand asks which hand the hero is interested in (rings.c gethand).
|
||||
func (g *RogueGame) gethand() int {
|
||||
for {
|
||||
if g.Options.Terse {
|
||||
g.msg("left or right ring? ")
|
||||
} else {
|
||||
g.msg("left hand or right hand? ")
|
||||
}
|
||||
c := g.readchar()
|
||||
if c == Escape {
|
||||
return -1
|
||||
}
|
||||
g.Msgs.Mpos = 0
|
||||
if c == 'l' || c == 'L' {
|
||||
return Left
|
||||
}
|
||||
if c == 'r' || c == 'R' {
|
||||
return Right
|
||||
}
|
||||
if g.Options.Terse {
|
||||
g.msg("L or R")
|
||||
} else {
|
||||
g.msg("please type L or R")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ringUses is the rings.c ring_eat static uses[] table: how much food each
|
||||
// ring type uses up per turn (negative = a 1-in-n chance of 1).
|
||||
var ringUses = [MaxRings]int{
|
||||
1, // R_PROTECT
|
||||
1, // R_ADDSTR
|
||||
1, // R_SUSTSTR
|
||||
-3, // R_SEARCH
|
||||
-5, // R_SEEINVIS
|
||||
0, // R_NOP
|
||||
0, // R_AGGR
|
||||
-3, // R_ADDHIT
|
||||
-3, // R_ADDDAM
|
||||
2, // R_REGEN
|
||||
-2, // R_DIGEST
|
||||
0, // R_TELEPORT
|
||||
1, // R_STEALTH
|
||||
1, // R_SUSTARM
|
||||
}
|
||||
|
||||
// ringEat reports how much food the ring on the given hand uses up
|
||||
// (rings.c ring_eat).
|
||||
func (g *RogueGame) ringEat(hand int) int {
|
||||
ring := g.Player.CurRing[hand]
|
||||
if ring == nil {
|
||||
return 0
|
||||
}
|
||||
eat := ringUses[ring.Which]
|
||||
if eat < 0 {
|
||||
if g.rnd(-eat) == 0 {
|
||||
eat = 1
|
||||
} else {
|
||||
eat = 0
|
||||
}
|
||||
}
|
||||
if ring.Which == RDigest {
|
||||
eat = -eat
|
||||
}
|
||||
return eat
|
||||
}
|
||||
|
||||
// ringNum prints ring bonuses (rings.c ring_num).
|
||||
func ringNum(g *RogueGame, obj *Object) string {
|
||||
if !obj.Flags.Has(IsKnow) {
|
||||
return ""
|
||||
}
|
||||
switch obj.Which {
|
||||
case RProtect, RAddStr, RAddDam, RAddHit:
|
||||
return fmt.Sprintf(" [%s]", num(obj.Arm, 0, Ring))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
377
game/rooms.go
Normal file
377
game/rooms.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package game
|
||||
|
||||
// rooms.c — create the layout for the new level.
|
||||
|
||||
// spot is the rooms.c SPOT position matrix for maze positions.
|
||||
type spot struct {
|
||||
nexits int
|
||||
exits [4]Coord
|
||||
used bool
|
||||
}
|
||||
|
||||
// mazeState is the rooms.c file-scope maze generation state.
|
||||
type mazeState struct {
|
||||
maxy, maxx, starty, startx int
|
||||
maze [NumLines/3 + 1][NumCols/3 + 1]spot
|
||||
}
|
||||
|
||||
const goldGrp = 1
|
||||
|
||||
// doRooms creates rooms and corridors with a connectivity graph (rooms.c
|
||||
// do_rooms).
|
||||
func (g *RogueGame) doRooms() {
|
||||
var bsze Coord // maximum room size
|
||||
bsze.X = NumCols / 3
|
||||
bsze.Y = NumLines / 3
|
||||
// Clear things for a new level
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
rp.GoldVal = 0
|
||||
rp.Exits = rp.Exits[:0]
|
||||
rp.Flags = 0
|
||||
}
|
||||
// Put the gone rooms, if any, on the level
|
||||
leftOut := g.rnd(4)
|
||||
for i := 0; i < leftOut; i++ {
|
||||
g.Level.Rooms[g.rndRoom()].Flags.Set(IsGone)
|
||||
}
|
||||
// dig and populate all the rooms on the level
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
// Find upper left corner of box that this room goes in
|
||||
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
|
||||
if rp.Flags.Has(IsGone) {
|
||||
// Place a gone room. Make certain that there is a blank line
|
||||
// for passage drawing.
|
||||
for {
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
|
||||
rp.Max.X = -NumCols
|
||||
rp.Max.Y = -NumLines
|
||||
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
// set room type
|
||||
if g.rnd(10) < g.Depth-1 {
|
||||
rp.Flags.Set(IsDark) // dark room
|
||||
if g.rnd(15) == 0 {
|
||||
rp.Flags = IsMaze // maze room
|
||||
}
|
||||
}
|
||||
// Find a place and size for a random room
|
||||
if rp.Flags.Has(IsMaze) {
|
||||
rp.Max.X = bsze.X - 1
|
||||
rp.Max.Y = bsze.Y - 1
|
||||
if rp.Pos.X = top.X; rp.Pos.X == 1 {
|
||||
rp.Pos.X = 0
|
||||
}
|
||||
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
|
||||
rp.Pos.Y++
|
||||
rp.Max.Y--
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
rp.Max.X = g.rnd(bsze.X-4) + 4
|
||||
rp.Max.Y = g.rnd(bsze.Y-4) + 4
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
|
||||
if rp.Pos.Y != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
g.drawRoom(rp)
|
||||
// Put the gold in
|
||||
if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) {
|
||||
gold := newObject()
|
||||
rp.GoldVal = g.goldCalc()
|
||||
gold.SetGoldVal(rp.GoldVal)
|
||||
rp.Gold, _ = g.findFloorIn(rp, 0, false)
|
||||
gold.Pos = rp.Gold
|
||||
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
|
||||
gold.Flags = IsMany
|
||||
gold.Group = goldGrp
|
||||
gold.Type = Gold
|
||||
attachObj(&g.Level.Objects, gold)
|
||||
}
|
||||
// Put the monster in
|
||||
prob := 25
|
||||
if rp.GoldVal > 0 {
|
||||
prob = 80
|
||||
}
|
||||
if g.rnd(100) < prob {
|
||||
tp := &Monster{}
|
||||
mp, _ := g.findFloorIn(rp, 0, true)
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
g.givePack(tp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawRoom draws a box around a room and lays down the floor for normal
|
||||
// rooms; for maze rooms, draws the maze (rooms.c draw_room).
|
||||
func (g *RogueGame) drawRoom(rp *Room) {
|
||||
if rp.Flags.Has(IsMaze) {
|
||||
g.doMaze(rp)
|
||||
return
|
||||
}
|
||||
g.vert(rp, rp.Pos.X) // Draw left side
|
||||
g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side
|
||||
g.horiz(rp, rp.Pos.Y) // Draw top
|
||||
g.horiz(rp, rp.Pos.Y+rp.Max.Y-1) // Draw bottom
|
||||
|
||||
// Put the floor down
|
||||
for y := rp.Pos.Y + 1; y < rp.Pos.Y+rp.Max.Y-1; y++ {
|
||||
for x := rp.Pos.X + 1; x < rp.Pos.X+rp.Max.X-1; x++ {
|
||||
g.Level.SetChar(y, x, Floor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vert draws a vertical line (rooms.c vert).
|
||||
func (g *RogueGame) vert(rp *Room, startx int) {
|
||||
for y := rp.Pos.Y + 1; y <= rp.Max.Y+rp.Pos.Y-1; y++ {
|
||||
g.Level.SetChar(y, startx, '|')
|
||||
}
|
||||
}
|
||||
|
||||
// horiz draws a horizontal line (rooms.c horiz).
|
||||
func (g *RogueGame) horiz(rp *Room, starty int) {
|
||||
for x := rp.Pos.X; x <= rp.Pos.X+rp.Max.X-1; x++ {
|
||||
g.Level.SetChar(starty, x, '-')
|
||||
}
|
||||
}
|
||||
|
||||
// doMaze digs a maze (rooms.c do_maze).
|
||||
func (g *RogueGame) doMaze(rp *Room) {
|
||||
m := &g.maze
|
||||
for y := range m.maze {
|
||||
for x := range m.maze[y] {
|
||||
m.maze[y][x].used = false
|
||||
m.maze[y][x].nexits = 0
|
||||
}
|
||||
}
|
||||
|
||||
m.maxy = rp.Max.Y
|
||||
m.maxx = rp.Max.X
|
||||
m.starty = rp.Pos.Y
|
||||
m.startx = rp.Pos.X
|
||||
starty := (g.rnd(rp.Max.Y) / 2) * 2
|
||||
startx := (g.rnd(rp.Max.X) / 2) * 2
|
||||
pos := Coord{Y: starty + m.starty, X: startx + m.startx}
|
||||
g.putpass(pos)
|
||||
g.dig(starty, startx)
|
||||
}
|
||||
|
||||
// dig digs out from around where we are now, if possible (rooms.c dig).
|
||||
func (g *RogueGame) dig(y, x int) {
|
||||
m := &g.maze
|
||||
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
|
||||
|
||||
for {
|
||||
cnt := 0
|
||||
var nexty, nextx int
|
||||
for _, cp := range del {
|
||||
newy := y + cp.Y
|
||||
newx := x + cp.X
|
||||
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
|
||||
continue
|
||||
}
|
||||
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPass) {
|
||||
continue
|
||||
}
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
nexty = newy
|
||||
nextx = newx
|
||||
}
|
||||
}
|
||||
if cnt == 0 {
|
||||
return
|
||||
}
|
||||
g.accntMaze(y, x, nexty, nextx)
|
||||
g.accntMaze(nexty, nextx, y, x)
|
||||
var pos Coord
|
||||
if nexty == y {
|
||||
pos.Y = y + m.starty
|
||||
if nextx-x < 0 {
|
||||
pos.X = nextx + m.startx + 1
|
||||
} else {
|
||||
pos.X = nextx + m.startx - 1
|
||||
}
|
||||
} else {
|
||||
pos.X = x + m.startx
|
||||
if nexty-y < 0 {
|
||||
pos.Y = nexty + m.starty + 1
|
||||
} else {
|
||||
pos.Y = nexty + m.starty - 1
|
||||
}
|
||||
}
|
||||
g.putpass(pos)
|
||||
pos.Y = nexty + m.starty
|
||||
pos.X = nextx + m.startx
|
||||
g.putpass(pos)
|
||||
g.dig(nexty, nextx)
|
||||
}
|
||||
}
|
||||
|
||||
// accntMaze accounts for maze exits (rooms.c accnt_maze).
|
||||
func (g *RogueGame) accntMaze(y, x, ny, nx int) {
|
||||
sp := &g.maze.maze[y][x]
|
||||
for i := 0; i < sp.nexits; i++ {
|
||||
if sp.exits[i].Y == ny && sp.exits[i].X == nx {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Note: the C original never increments nexits here (a latent bug it
|
||||
// inherits); it stores the exit in the next slot and relies on the
|
||||
// slot staying zero-counted. We reproduce the store-without-count.
|
||||
if sp.nexits < len(sp.exits) {
|
||||
sp.exits[sp.nexits] = Coord{Y: ny, X: nx}
|
||||
}
|
||||
}
|
||||
|
||||
// rndPos picks a random spot in a room (rooms.c rnd_pos).
|
||||
func (g *RogueGame) rndPos(rp *Room) Coord {
|
||||
var cp Coord
|
||||
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
|
||||
cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1
|
||||
return cp
|
||||
}
|
||||
|
||||
// findFloor finds a valid floor spot, picking a new random room each time
|
||||
// around the loop (rooms.c find_floor with rp == NULL).
|
||||
func (g *RogueGame) findFloor(rp *Room, limit int, monst bool) (Coord, bool) {
|
||||
return g.findFloorImpl(rp, limit, monst, rp == nil)
|
||||
}
|
||||
|
||||
// findFloorIn finds a valid floor spot in this room (rooms.c find_floor
|
||||
// with a specific room).
|
||||
func (g *RogueGame) findFloorIn(rp *Room, limit int, monst bool) (Coord, bool) {
|
||||
return g.findFloorImpl(rp, limit, monst, false)
|
||||
}
|
||||
|
||||
func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) {
|
||||
var compchar byte
|
||||
if !pickroom {
|
||||
compchar = Floor
|
||||
if rp.Flags.Has(IsMaze) {
|
||||
compchar = Passage
|
||||
}
|
||||
}
|
||||
cnt := limit
|
||||
for {
|
||||
if limit != 0 {
|
||||
if cnt--; cnt == -1 {
|
||||
return Coord{}, false
|
||||
}
|
||||
}
|
||||
if pickroom {
|
||||
rp = &g.Level.Rooms[g.rndRoom()]
|
||||
compchar = Floor
|
||||
if rp.Flags.Has(IsMaze) {
|
||||
compchar = Passage
|
||||
}
|
||||
}
|
||||
cp := g.rndPos(rp)
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
if monst {
|
||||
if pp.Monst == nil && stepOk(pp.Ch) {
|
||||
return cp, true
|
||||
}
|
||||
} else if pp.Ch == compchar {
|
||||
return cp, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enterRoom is the code executed whenever you appear in a room (rooms.c
|
||||
// enter_room).
|
||||
func (g *RogueGame) enterRoom(cp Coord) {
|
||||
p := &g.Player
|
||||
rp := g.roomin(cp)
|
||||
p.Room = rp
|
||||
g.doorOpen(rp)
|
||||
if !rp.Flags.Has(IsDark) && !p.On(IsBlind) {
|
||||
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
||||
g.move(y, rp.Pos.X)
|
||||
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
ch := g.Level.Char(y, x)
|
||||
if tp == nil {
|
||||
if g.inch() != ch {
|
||||
g.addch(ch)
|
||||
} else {
|
||||
g.move(y, x+1)
|
||||
}
|
||||
} else {
|
||||
tp.OldCh = ch
|
||||
if !g.seeMonst(tp) {
|
||||
if p.On(SeeMonst) {
|
||||
g.standout()
|
||||
g.addch(tp.Disguise)
|
||||
g.standend()
|
||||
} else {
|
||||
g.addch(ch)
|
||||
}
|
||||
} else {
|
||||
g.addch(tp.Disguise)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// leaveRoom is the code for when we exit a room (rooms.c leave_room).
|
||||
func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
p := &g.Player
|
||||
rp := p.Room
|
||||
|
||||
if rp.Flags.Has(IsMaze) {
|
||||
return
|
||||
}
|
||||
|
||||
var floor byte
|
||||
switch {
|
||||
case rp.Flags.Has(IsGone):
|
||||
floor = Passage
|
||||
case !rp.Flags.Has(IsDark) || p.On(IsBlind):
|
||||
floor = Floor
|
||||
default:
|
||||
floor = ' '
|
||||
}
|
||||
|
||||
p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPNum]
|
||||
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
||||
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
||||
g.move(y, x)
|
||||
switch ch := g.inch(); ch {
|
||||
case Floor:
|
||||
if floor == ' ' {
|
||||
g.addch(' ')
|
||||
}
|
||||
default:
|
||||
// to check for monster, we have to strip out the standout
|
||||
// bit (our Window returns the bare character already)
|
||||
if isUpper(ch) {
|
||||
if p.On(SeeMonst) {
|
||||
g.standout()
|
||||
g.addch(ch)
|
||||
g.standend()
|
||||
break
|
||||
}
|
||||
pp := g.Level.At(y, x)
|
||||
if pp.Ch == Door {
|
||||
g.addch(Door)
|
||||
} else {
|
||||
g.addch(floor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g.doorOpen(rp)
|
||||
}
|
||||
198
game/screen.go
Normal file
198
game/screen.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The screen layer replaces curses. Game code draws into Window cell
|
||||
// buffers (stdscr and the hw scratch window); a Terminal implementation
|
||||
// blits them to a real device. Tests run with a scripted Terminal (or none
|
||||
// at all), which is also how the "screen is a data structure" idiom —
|
||||
// C code reading back what it drew with inch() — stays intact headlessly.
|
||||
|
||||
// Terminal is the physical device: a tcell screen in the real game, a
|
||||
// script in tests.
|
||||
type Terminal interface {
|
||||
// Render blits the window to the device.
|
||||
Render(w *Window)
|
||||
// ReadChar blocks for the next key, translated to Rogue's input bytes
|
||||
// (arrows become hjkl, control keys their C0 codes).
|
||||
ReadChar() byte
|
||||
}
|
||||
|
||||
// cell is one screen position.
|
||||
type cell struct {
|
||||
ch byte
|
||||
standout bool
|
||||
}
|
||||
|
||||
// Window is an in-memory curses window: a cell grid with a cursor and a
|
||||
// standout attribute.
|
||||
type Window struct {
|
||||
rows, cols int
|
||||
cells []cell
|
||||
cy, cx int
|
||||
standout bool
|
||||
}
|
||||
|
||||
// NewWindow returns a cleared window.
|
||||
func NewWindow(rows, cols int) *Window {
|
||||
w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)}
|
||||
w.Clear()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
|
||||
|
||||
// Move positions the cursor (curses move/wmove).
|
||||
func (w *Window) Move(y, x int) { w.cy, w.cx = y, x }
|
||||
|
||||
// GetYX reports the cursor position (curses getyx).
|
||||
func (w *Window) GetYX() (y, x int) { return w.cy, w.cx }
|
||||
|
||||
// AddCh writes a character at the cursor and advances it (curses addch).
|
||||
func (w *Window) AddCh(ch byte) {
|
||||
if ch == '\n' {
|
||||
w.cy, w.cx = w.cy+1, 0
|
||||
return
|
||||
}
|
||||
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
|
||||
return
|
||||
}
|
||||
*w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout}
|
||||
if w.cx++; w.cx >= w.cols {
|
||||
w.cx = 0
|
||||
if w.cy < w.rows-1 {
|
||||
w.cy++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddStr writes a string at the cursor (curses addstr).
|
||||
func (w *Window) AddStr(s string) {
|
||||
for i := 0; i < len(s); i++ {
|
||||
w.AddCh(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
// MvAddCh moves then writes (curses mvaddch).
|
||||
func (w *Window) MvAddCh(y, x int, ch byte) {
|
||||
w.Move(y, x)
|
||||
w.AddCh(ch)
|
||||
}
|
||||
|
||||
// MvAddStr moves then writes (curses mvaddstr).
|
||||
func (w *Window) MvAddStr(y, x int, s string) {
|
||||
w.Move(y, x)
|
||||
w.AddStr(s)
|
||||
}
|
||||
|
||||
// Printw writes formatted text at the cursor (curses printw).
|
||||
func (w *Window) Printw(format string, a ...any) {
|
||||
w.AddStr(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// MvPrintw moves then writes formatted text (curses mvprintw).
|
||||
func (w *Window) MvPrintw(y, x int, format string, a ...any) {
|
||||
w.Move(y, x)
|
||||
w.Printw(format, a...)
|
||||
}
|
||||
|
||||
// Inch returns the character under the cursor (curses inch, sans
|
||||
// attributes — the C code always strips them with CCHAR).
|
||||
func (w *Window) Inch() byte {
|
||||
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
|
||||
return ' '
|
||||
}
|
||||
return w.at(w.cy, w.cx).ch
|
||||
}
|
||||
|
||||
// MvInch moves then reads (curses mvinch).
|
||||
func (w *Window) MvInch(y, x int) byte {
|
||||
w.Move(y, x)
|
||||
return w.Inch()
|
||||
}
|
||||
|
||||
// Standout sets or clears the standout attribute for subsequent writes
|
||||
// (curses standout/standend).
|
||||
func (w *Window) Standout(on bool) { w.standout = on }
|
||||
|
||||
// Clear blanks the window and homes the cursor (curses clear/wclear).
|
||||
func (w *Window) Clear() {
|
||||
for i := range w.cells {
|
||||
w.cells[i] = cell{ch: ' '}
|
||||
}
|
||||
w.cy, w.cx = 0, 0
|
||||
}
|
||||
|
||||
// Clrtoeol blanks from the cursor to the end of the line (curses clrtoeol).
|
||||
func (w *Window) Clrtoeol() {
|
||||
if w.cy < 0 || w.cy >= w.rows {
|
||||
return
|
||||
}
|
||||
for x := w.cx; x < w.cols; x++ {
|
||||
*w.at(w.cy, x) = cell{ch: ' '}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyFrom copies another window's contents (curses overwrite).
|
||||
func (w *Window) CopyFrom(src *Window) {
|
||||
copy(w.cells, src.cells)
|
||||
}
|
||||
|
||||
// Line returns row y as a trimmed string; used by tests and the death/
|
||||
// victory screens.
|
||||
func (w *Window) Line(y int) string {
|
||||
buf := make([]byte, w.cols)
|
||||
for x := 0; x < w.cols; x++ {
|
||||
buf[x] = w.at(y, x).ch
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// Screen bundles the two windows the game draws on with the device that
|
||||
// shows them.
|
||||
type Screen struct {
|
||||
term Terminal
|
||||
Std *Window // stdscr: the dungeon view
|
||||
Hw *Window // hw: the scratch window for overlays
|
||||
}
|
||||
|
||||
// NewScreen builds the standard 24x80 game screen.
|
||||
func NewScreen(term Terminal) *Screen {
|
||||
return &Screen{
|
||||
term: term,
|
||||
Std: NewWindow(NumLines, NumCols),
|
||||
Hw: NewWindow(NumLines, NumCols),
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh pushes stdscr to the device (curses refresh).
|
||||
func (s *Screen) Refresh() {
|
||||
if s.term != nil {
|
||||
s.term.Render(s.Std)
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
|
||||
func (s *Screen) RefreshWin(w *Window) {
|
||||
if s.term != nil {
|
||||
s.term.Render(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Thin RogueGame wrappers so ported bodies keep their curses shape.
|
||||
|
||||
func (g *RogueGame) move(y, x int) { g.scr.Std.Move(y, x) }
|
||||
func (g *RogueGame) addch(ch byte) { g.scr.Std.AddCh(ch) }
|
||||
func (g *RogueGame) addstr(s string) { g.scr.Std.AddStr(s) }
|
||||
func (g *RogueGame) mvaddch(y, x int, c byte) { g.scr.Std.MvAddCh(y, x, c) }
|
||||
func (g *RogueGame) mvaddstr(y, x int, s string) {
|
||||
g.scr.Std.MvAddStr(y, x, s)
|
||||
}
|
||||
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printw(f, a...) }
|
||||
func (g *RogueGame) inch() byte { return g.scr.Std.Inch() }
|
||||
func (g *RogueGame) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) }
|
||||
func (g *RogueGame) standout() { g.scr.Std.Standout(true) }
|
||||
func (g *RogueGame) standend() { g.scr.Std.Standout(false) }
|
||||
func (g *RogueGame) clear() { g.scr.Std.Clear() }
|
||||
func (g *RogueGame) clrtoeol() { g.scr.Std.Clrtoeol() }
|
||||
func (g *RogueGame) refresh() { g.scr.Refresh() }
|
||||
35
game/sticks.go
Normal file
35
game/sticks.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// sticks.c — wand and staff setup and naming. Zapping (do_zap, drain,
|
||||
// fire_bolt) arrives with the combat phase.
|
||||
|
||||
// fixStick sets up a new wand or staff (sticks.c fix_stick).
|
||||
func (g *RogueGame) fixStick(cur *Object) {
|
||||
if g.Items.WandType[cur.Which] == "staff" {
|
||||
cur.Damage = "2x3"
|
||||
} else {
|
||||
cur.Damage = "1x1"
|
||||
}
|
||||
cur.HurlDmg = "1x1"
|
||||
|
||||
switch cur.Which {
|
||||
case WsLight:
|
||||
cur.SetCharges(g.rnd(10) + 10)
|
||||
default:
|
||||
cur.SetCharges(g.rnd(5) + 3)
|
||||
}
|
||||
}
|
||||
|
||||
// chargeStr returns the charge-count suffix for an identified stick
|
||||
// (sticks.c charge_str).
|
||||
func chargeStr(g *RogueGame, obj *Object) string {
|
||||
if !obj.Flags.Has(IsKnow) {
|
||||
return ""
|
||||
}
|
||||
if g.Options.Terse {
|
||||
return fmt.Sprintf(" [%d]", obj.Charges())
|
||||
}
|
||||
return fmt.Sprintf(" [%d charges]", obj.Charges())
|
||||
}
|
||||
502
game/things.go
Normal file
502
game/things.go
Normal file
@@ -0,0 +1,502 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// things.c — item naming, random item creation, and the discovery list.
|
||||
|
||||
// invName returns the name of something as it would appear in an inventory
|
||||
// (things.c inv_name).
|
||||
func (g *RogueGame) invName(obj *Object, drop bool) string {
|
||||
var pb strings.Builder
|
||||
which := obj.Which
|
||||
it := &g.Items
|
||||
switch obj.Type {
|
||||
case Potion:
|
||||
g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr)
|
||||
case Ring:
|
||||
g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum)
|
||||
case Stick:
|
||||
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr)
|
||||
case Scroll:
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("A scroll ")
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d scrolls ", obj.Count)
|
||||
}
|
||||
op := &it.Scrolls[which]
|
||||
if op.Know {
|
||||
fmt.Fprintf(&pb, "of %s", op.Name)
|
||||
} else if op.Guess != "" {
|
||||
fmt.Fprintf(&pb, "called %s", op.Guess)
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which])
|
||||
}
|
||||
case Food:
|
||||
if which == 1 {
|
||||
if obj.Count == 1 {
|
||||
fmt.Fprintf(&pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d %ss", obj.Count, g.Fruit)
|
||||
}
|
||||
} else {
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("Some food")
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "%d rations of food", obj.Count)
|
||||
}
|
||||
}
|
||||
case Weapon:
|
||||
sp := it.Weapons[which].Name
|
||||
if obj.Count > 1 {
|
||||
fmt.Fprintf(&pb, "%d ", obj.Count)
|
||||
} else {
|
||||
fmt.Fprintf(&pb, "A%s ", vowelstr(sp))
|
||||
}
|
||||
if obj.Flags.Has(IsKnow) {
|
||||
fmt.Fprintf(&pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
if obj.Count > 1 {
|
||||
pb.WriteString("s")
|
||||
}
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(&pb, " called %s", obj.Label)
|
||||
}
|
||||
case Armor:
|
||||
sp := it.Armors[which].Name
|
||||
if obj.Flags.Has(IsKnow) {
|
||||
fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.Arm, 0, Armor), sp)
|
||||
if !g.Options.Terse {
|
||||
pb.WriteString("protection ")
|
||||
}
|
||||
fmt.Fprintf(&pb, "%d]", 10-obj.Arm)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(&pb, " called %s", obj.Label)
|
||||
}
|
||||
case Amulet:
|
||||
pb.WriteString("The Amulet of Yendor")
|
||||
case Gold:
|
||||
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal())
|
||||
}
|
||||
|
||||
out := pb.String()
|
||||
if g.InvDescribe {
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor {
|
||||
out += " (being worn)"
|
||||
}
|
||||
if obj == p.CurWeapon {
|
||||
out += " (weapon in hand)"
|
||||
}
|
||||
if obj == p.CurRing[Left] {
|
||||
out += " (on left hand)"
|
||||
} else if obj == p.CurRing[Right] {
|
||||
out += " (on right hand)"
|
||||
}
|
||||
}
|
||||
if out != "" {
|
||||
if drop && isUpper(out[0]) {
|
||||
out = string(toLower(out[0])) + out[1:]
|
||||
} else if !drop && isLower(out[0]) {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dropIt puts something down (things.c drop; renamed to avoid the
|
||||
// leavePack/detach vocabulary collision).
|
||||
func (g *RogueGame) dropIt() {
|
||||
p := &g.Player
|
||||
ch := g.Level.Char(p.Pos.Y, p.Pos.X)
|
||||
if ch != Floor && ch != Passage {
|
||||
g.After = false
|
||||
g.msg("there is something there already")
|
||||
return
|
||||
}
|
||||
obj := g.getItem("drop", 0)
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if !g.dropCheck(obj) {
|
||||
return
|
||||
}
|
||||
obj = g.leavePack(obj, true, !IsMult(obj.Type))
|
||||
// Link it into the level object list
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Type)
|
||||
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
|
||||
obj.Pos = p.Pos
|
||||
if obj.Type == Amulet {
|
||||
g.HasAmulet = false
|
||||
}
|
||||
g.msg("dropped %s", g.invName(obj, true))
|
||||
}
|
||||
|
||||
// dropCheck does special checks for dropping or unwielding|unwearing|
|
||||
// unringing (things.c dropcheck).
|
||||
func (g *RogueGame) dropCheck(obj *Object) bool {
|
||||
if obj == nil {
|
||||
return true
|
||||
}
|
||||
p := &g.Player
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] {
|
||||
return true
|
||||
}
|
||||
if obj.Flags.Has(IsCursed) {
|
||||
g.msg("you can't. It appears to be cursed")
|
||||
return false
|
||||
}
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
} else if obj == p.CurArmor {
|
||||
g.wasteTime()
|
||||
p.CurArmor = nil
|
||||
} else {
|
||||
hand := Right
|
||||
if obj == p.CurRing[Left] {
|
||||
hand = Left
|
||||
}
|
||||
p.CurRing[hand] = nil
|
||||
switch obj.Which {
|
||||
case RAddStr:
|
||||
g.chgStr(-obj.Arm)
|
||||
case RSeeInvis:
|
||||
g.unsee(0)
|
||||
g.Extinguish(DUnsee)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// newThing returns a new random thing for the dungeon (things.c new_thing).
|
||||
func (g *RogueGame) newThing() *Object {
|
||||
cur := newObject()
|
||||
cur.Damage = "0x0"
|
||||
cur.HurlDmg = "0x0"
|
||||
cur.Arm = 11
|
||||
cur.Count = 1
|
||||
|
||||
// Decide what kind of object it will be; if we haven't had food for a
|
||||
// while, let it be food.
|
||||
var kind int
|
||||
if g.Player.NoFood > 3 {
|
||||
kind = 2
|
||||
} else {
|
||||
kind = pickOne(g, g.Items.Things[:])
|
||||
}
|
||||
switch kind {
|
||||
case 0:
|
||||
cur.Type = Potion
|
||||
cur.Which = pickOne(g, g.Items.Potions[:])
|
||||
case 1:
|
||||
cur.Type = Scroll
|
||||
cur.Which = pickOne(g, g.Items.Scrolls[:])
|
||||
case 2:
|
||||
cur.Type = Food
|
||||
g.Player.NoFood = 0
|
||||
if g.rnd(10) != 0 {
|
||||
cur.Which = 0
|
||||
} else {
|
||||
cur.Which = 1
|
||||
}
|
||||
case 3:
|
||||
g.initWeapon(cur, pickOne(g, g.Items.Weapons[:MaxWeapons]))
|
||||
if r := g.rnd(100); r < 10 {
|
||||
cur.Flags.Set(IsCursed)
|
||||
cur.HPlus -= g.rnd(3) + 1
|
||||
} else if r < 15 {
|
||||
cur.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
case 4:
|
||||
cur.Type = Armor
|
||||
cur.Which = pickOne(g, g.Items.Armors[:])
|
||||
cur.Arm = aClass[cur.Which]
|
||||
if r := g.rnd(100); r < 20 {
|
||||
cur.Flags.Set(IsCursed)
|
||||
cur.Arm += g.rnd(3) + 1
|
||||
} else if r < 28 {
|
||||
cur.Arm -= g.rnd(3) + 1
|
||||
}
|
||||
case 5:
|
||||
cur.Type = Ring
|
||||
cur.Which = pickOne(g, g.Items.Rings[:])
|
||||
switch cur.Which {
|
||||
case RAddStr, RProtect, RAddHit, RAddDam:
|
||||
if cur.Arm = g.rnd(3); cur.Arm == 0 {
|
||||
cur.Arm = -1
|
||||
cur.Flags.Set(IsCursed)
|
||||
}
|
||||
case RAggr, RTeleport:
|
||||
cur.Flags.Set(IsCursed)
|
||||
}
|
||||
case 6:
|
||||
cur.Type = Stick
|
||||
cur.Which = pickOne(g, g.Items.Sticks[:])
|
||||
g.fixStick(cur)
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
// pickOne picks an item out of a list of possible objects using their
|
||||
// cumulative probabilities (things.c pick_one).
|
||||
func pickOne(g *RogueGame, info []ObjInfo) int {
|
||||
i := g.rnd(100)
|
||||
for idx := range info {
|
||||
if i < info[idx].Prob {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
return 0 // bad pick_one: C resets to the start of the table
|
||||
}
|
||||
|
||||
// invPage is the things.c static pagination state for the discovery/
|
||||
// inventory list windows (line_cnt, newpage, lastfmt/lastarg, maxlen).
|
||||
type invPage struct {
|
||||
lineCnt int
|
||||
newpage bool
|
||||
lastLine string
|
||||
maxlen int
|
||||
init bool
|
||||
}
|
||||
|
||||
// discovered lists what the player has found of a certain type
|
||||
// (things.c discovered).
|
||||
func (g *RogueGame) discovered() {
|
||||
var ch byte
|
||||
for {
|
||||
discList := false
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("for ")
|
||||
}
|
||||
g.addmsg("what type")
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" of object do you want a list")
|
||||
}
|
||||
g.msg("? (* for all)")
|
||||
ch = g.readchar()
|
||||
switch ch {
|
||||
case Escape:
|
||||
g.msg("")
|
||||
return
|
||||
case Potion, Scroll, Ring, Stick, '*':
|
||||
discList = true
|
||||
default:
|
||||
if g.Options.Terse {
|
||||
g.msg("Not a type")
|
||||
} else {
|
||||
g.msg("Please type one of %c%c%c%c (ESCAPE to quit)",
|
||||
Potion, Scroll, Ring, Stick)
|
||||
}
|
||||
}
|
||||
if discList {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ch == '*' {
|
||||
g.printDisc(Potion)
|
||||
g.addLine("")
|
||||
g.printDisc(Scroll)
|
||||
g.addLine("")
|
||||
g.printDisc(Ring)
|
||||
g.addLine("")
|
||||
g.printDisc(Stick)
|
||||
g.endLine()
|
||||
} else {
|
||||
g.printDisc(ch)
|
||||
g.endLine()
|
||||
}
|
||||
}
|
||||
|
||||
// printDisc prints what we've discovered of the given type
|
||||
// (things.c print_disc).
|
||||
func (g *RogueGame) printDisc(typ byte) {
|
||||
var info []ObjInfo
|
||||
switch typ {
|
||||
case Scroll:
|
||||
info = g.Items.Scrolls[:]
|
||||
case Potion:
|
||||
info = g.Items.Potions[:]
|
||||
case Ring:
|
||||
info = g.Items.Rings[:]
|
||||
case Stick:
|
||||
info = g.Items.Sticks[:]
|
||||
}
|
||||
order := make([]int, len(info))
|
||||
g.setOrder(order)
|
||||
obj := Object{Count: 1}
|
||||
numFound := 0
|
||||
for i := range info {
|
||||
if info[order[i]].Know || info[order[i]].Guess != "" {
|
||||
obj.Type = typ
|
||||
obj.Which = order[i]
|
||||
g.addLine("%s", g.invName(&obj, false))
|
||||
numFound++
|
||||
}
|
||||
}
|
||||
if numFound == 0 {
|
||||
g.addLine("%s", g.nothing(typ))
|
||||
}
|
||||
}
|
||||
|
||||
// setOrder shuffles the display order for the discovery list
|
||||
// (things.c set_order).
|
||||
func (g *RogueGame) setOrder(order []int) {
|
||||
for i := range order {
|
||||
order[i] = i
|
||||
}
|
||||
for i := len(order); i > 0; i-- {
|
||||
r := g.rnd(i)
|
||||
order[i-1], order[r] = order[r], order[i-1]
|
||||
}
|
||||
}
|
||||
|
||||
// addLine adds a line to the list of discoveries (things.c add_line). A
|
||||
// format of exactly "\x00" is the C fmt==NULL page-flush sentinel — use
|
||||
// flushLine() for that.
|
||||
const flushSentinel = "\x00"
|
||||
|
||||
func (g *RogueGame) addLine(format string, a ...any) int {
|
||||
pg := &g.invPage
|
||||
prompt := "--Press space to continue--"
|
||||
isFlush := format == flushSentinel
|
||||
var line string
|
||||
if !isFlush {
|
||||
line = fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
if pg.lineCnt == 0 {
|
||||
g.scr.Hw.Clear()
|
||||
if g.Options.InvType == InvSlow {
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
}
|
||||
if g.Options.InvType == InvSlow {
|
||||
if !isFlush && line != "" {
|
||||
if g.msg("%s", line) == Escape {
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
pg.lineCnt++
|
||||
} else {
|
||||
if !pg.init {
|
||||
pg.maxlen = len(prompt)
|
||||
pg.init = true
|
||||
}
|
||||
if pg.lineCnt >= NumLines-1 || isFlush {
|
||||
if g.Options.InvType == InvOver && isFlush && !pg.newpage {
|
||||
// Overlay the accumulated list in a box at the top right
|
||||
// of the screen, prompt, and restore what was beneath.
|
||||
g.msg("")
|
||||
g.refresh()
|
||||
saved := NewWindow(NumLines, NumCols)
|
||||
saved.CopyFrom(g.scr.Std)
|
||||
lx := NumCols - pg.maxlen - 2
|
||||
for y := 0; y <= pg.lineCnt; y++ {
|
||||
for x := 0; x <= pg.maxlen; x++ {
|
||||
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
|
||||
}
|
||||
}
|
||||
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
|
||||
g.refresh()
|
||||
g.waitFor(' ')
|
||||
g.scr.Std.CopyFrom(saved)
|
||||
g.refresh()
|
||||
} else {
|
||||
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
|
||||
g.scr.RefreshWin(g.scr.Hw)
|
||||
g.waitFor(' ')
|
||||
g.scr.Hw.Clear()
|
||||
g.refresh()
|
||||
}
|
||||
pg.newpage = true
|
||||
pg.lineCnt = 0
|
||||
pg.maxlen = len(prompt)
|
||||
}
|
||||
if !isFlush && !(pg.lineCnt == 0 && line == "") {
|
||||
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
|
||||
pg.lineCnt++
|
||||
if pg.maxlen < len(line) {
|
||||
pg.maxlen = len(line)
|
||||
}
|
||||
pg.lastLine = line
|
||||
}
|
||||
}
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// flushLine is add_line(NULL): force out the accumulated page.
|
||||
func (g *RogueGame) flushLine() int { return g.addLine(flushSentinel) }
|
||||
|
||||
// endLine ends the list of lines (things.c end_line).
|
||||
func (g *RogueGame) endLine() {
|
||||
pg := &g.invPage
|
||||
if g.Options.InvType != InvSlow {
|
||||
if pg.lineCnt == 1 && !pg.newpage {
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", pg.lastLine)
|
||||
} else {
|
||||
g.flushLine()
|
||||
}
|
||||
}
|
||||
pg.lineCnt = 0
|
||||
pg.newpage = false
|
||||
}
|
||||
|
||||
// nothing builds the "nothing found" message for a type (things.c nothing).
|
||||
func (g *RogueGame) nothing(typ byte) string {
|
||||
var out string
|
||||
if g.Options.Terse {
|
||||
out = "Nothing"
|
||||
} else {
|
||||
out = "Haven't discovered anything"
|
||||
}
|
||||
if typ != '*' {
|
||||
var tystr string
|
||||
switch typ {
|
||||
case Potion:
|
||||
tystr = "potion"
|
||||
case Scroll:
|
||||
tystr = "scroll"
|
||||
case Ring:
|
||||
tystr = "ring"
|
||||
case Stick:
|
||||
tystr = "stick"
|
||||
}
|
||||
out += fmt.Sprintf(" about any %ss", tystr)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// nameit gives the proper name to a potion, stick, or ring
|
||||
// (things.c nameit).
|
||||
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
|
||||
op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
|
||||
if op.Know || op.Guess != "" {
|
||||
if obj.Count == 1 {
|
||||
fmt.Fprintf(pb, "A %s ", typ)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d %ss ", obj.Count, typ)
|
||||
}
|
||||
if op.Know {
|
||||
fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "called %s%s(%s)", op.Guess, prfunc(g, obj), which)
|
||||
}
|
||||
} else if obj.Count == 1 {
|
||||
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
|
||||
}
|
||||
}
|
||||
|
||||
// nullstr returns an empty string (things.c nullstr).
|
||||
func nullstr(*RogueGame, *Object) string { return "" }
|
||||
86
game/weapons.go
Normal file
86
game/weapons.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// weapons.c — weapon initialization and naming. The throwing half
|
||||
// (missile, do_motion, hit_monster) arrives with the combat phase.
|
||||
|
||||
const noWeapon = -1
|
||||
|
||||
// initWeaps is the weapons.c init_dam[] table.
|
||||
var initWeaps = [MaxWeapons]struct {
|
||||
dam string // damage when wielded
|
||||
hrl string // damage when thrown
|
||||
launch int // launching weapon
|
||||
flags ObjFlags
|
||||
}{
|
||||
{"2x4", "1x3", noWeapon, 0}, // Mace
|
||||
{"3x4", "1x2", noWeapon, 0}, // Long sword
|
||||
{"1x1", "1x1", noWeapon, 0}, // Bow
|
||||
{"1x1", "2x3", Bow, IsMany | IsMissl}, // Arrow
|
||||
{"1x6", "1x4", noWeapon, IsMissl}, // Dagger
|
||||
{"4x4", "1x2", noWeapon, 0}, // 2h sword
|
||||
{"1x1", "1x3", noWeapon, IsMany | IsMissl}, // Dart
|
||||
{"1x2", "2x4", noWeapon, IsMany | IsMissl}, // Shuriken
|
||||
{"2x3", "1x6", noWeapon, IsMissl}, // Spear
|
||||
}
|
||||
|
||||
// initWeapon sets up a new weapon (weapons.c init_weapon).
|
||||
func (g *RogueGame) initWeapon(weap *Object, which int) {
|
||||
iwp := &initWeaps[which]
|
||||
weap.Type = Weapon
|
||||
weap.Which = which
|
||||
weap.Damage = iwp.dam
|
||||
weap.HurlDmg = iwp.hrl
|
||||
weap.Launch = iwp.launch
|
||||
weap.Flags = iwp.flags
|
||||
weap.HPlus = 0
|
||||
weap.DPlus = 0
|
||||
if which == Dagger {
|
||||
weap.Count = g.rnd(4) + 2
|
||||
weap.Group = g.Items.Group
|
||||
g.Items.Group++
|
||||
} else if weap.Flags.Has(IsMany) {
|
||||
weap.Count = g.rnd(8) + 8
|
||||
weap.Group = g.Items.Group
|
||||
g.Items.Group++
|
||||
} else {
|
||||
weap.Count = 1
|
||||
weap.Group = 0
|
||||
}
|
||||
}
|
||||
|
||||
// num formats a hit/damage or armor bonus string (weapons.c num).
|
||||
func num(n1, n2 int, typ byte) string {
|
||||
out := fmt.Sprintf("%+d", n1)
|
||||
if typ == Weapon {
|
||||
out += fmt.Sprintf(",%+d", n2)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fallpos picks a random empty position around (pos) for a dropped item
|
||||
// (weapons.c fallpos).
|
||||
func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
|
||||
var newpos Coord
|
||||
cnt := 0
|
||||
for y := pos.Y - 1; y <= pos.Y+1; y++ {
|
||||
for x := pos.X - 1; x <= pos.X+1; x++ {
|
||||
// check to make certain the spot is empty, if it is, put the
|
||||
// object there, set it in the level list and re-draw the room
|
||||
// if he can see it
|
||||
if (y == g.Player.Pos.Y && x == g.Player.Pos.X) ||
|
||||
y < 0 || x < 0 {
|
||||
continue
|
||||
}
|
||||
ch := g.Level.Char(y, x)
|
||||
if ch == Floor || ch == Passage {
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
newpos.Y = y
|
||||
newpos.X = x
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return newpos, cnt != 0
|
||||
}
|
||||
Reference in New Issue
Block a user