go: port combat, chase driver, traps, zapping, death and scores
- fight.c in full: fight/attack with all eight special monster attacks (aquator rust, ice freeze, rattlesnake poison, wraith/vampire drains, flytrap hold, leprechaun/nymph theft), swing, roll_em dice parsing (with a C-semantics atoi — Go's strconv rejects '1x4/...'), hit/miss message variants, killed, remove_mon - chase.c completed: runners, move_monst, relocate, do_chase with dragon breath, chase target selection (C's dead oroom comparison in relocate is preserved as-is) - move.c in full: do_move with passgo turning, all eight traps, rndmove, rust_armor - sticks.c completed: do_zap (all 14 sticks), drain, fire_bolt with wall bounces; weapons.c completed: missile/do_motion/fall/wield - rip.c: death tombstone, total_winner, killname; C exit() becomes a gameEnd panic that Run will recover - score.go: top-ten scoreboard as a gob file with lock-file protocol replacing the XOR-encrypted C format - wizard.c: whatis/set_know/teleport; daemons.c stomach - monster bestiary moved to per-game state (C mutates the flytrap damage string during play) Tests: combat math, kill/removal bookkeeping, monster chase pursuit, death unwinding, scripted-input headless terminal.
This commit is contained in:
265
game/chase.go
265
game/chase.go
@@ -1,8 +1,265 @@
|
||||
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.
|
||||
// chase.c — code for one creature to chase another.
|
||||
|
||||
// dragonShot: one chance in DRAGONSHOT that a dragon will flame.
|
||||
const dragonShot = 5
|
||||
|
||||
// runners makes all the running monsters move (chase.c runners).
|
||||
func (g *RogueGame) runners(int) {
|
||||
list := append([]*Monster(nil), g.Level.Monsters...)
|
||||
for _, tp := range list {
|
||||
if !tp.On(IsHeld) && tp.On(IsRun) {
|
||||
origPos := tp.Pos
|
||||
wastarget := tp.On(IsTarget)
|
||||
if g.moveMonst(tp) == -1 {
|
||||
continue
|
||||
}
|
||||
if tp.On(IsFly) && distCp(g.Player.Pos, tp.Pos) >= 3 {
|
||||
if g.moveMonst(tp) == -1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if wastarget && origPos != tp.Pos {
|
||||
tp.Flags.Clear(IsTarget)
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
}
|
||||
}
|
||||
|
||||
// moveMonst executes a single turn of running for a monster (chase.c
|
||||
// move_monst). Returns -1 if the monster died or left the level.
|
||||
func (g *RogueGame) moveMonst(tp *Monster) int {
|
||||
if !tp.On(IsSlow) || tp.Turn {
|
||||
if g.doChase(tp) == -1 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
if tp.On(IsHaste) {
|
||||
if g.doChase(tp) == -1 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
tp.Turn = !tp.Turn
|
||||
return 0
|
||||
}
|
||||
|
||||
// relocate makes the monster's new location be the specified one, updating
|
||||
// all the relevant state (chase.c relocate).
|
||||
func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
|
||||
if newLoc != th.Pos {
|
||||
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
|
||||
th.Room = g.roomin(newLoc)
|
||||
g.setOldch(th, newLoc)
|
||||
oroom := th.Room
|
||||
g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil)
|
||||
|
||||
if oroom != th.Room {
|
||||
th.Dest = g.findDest(th)
|
||||
}
|
||||
th.Pos = newLoc
|
||||
g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th)
|
||||
}
|
||||
g.move(newLoc.Y, newLoc.X)
|
||||
if g.seeMonst(th) {
|
||||
g.addch(th.Disguise)
|
||||
} else if g.Player.On(SeeMonst) {
|
||||
g.standout()
|
||||
g.addch(th.Type)
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
// doChase makes one thing chase another (chase.c do_chase). Returns -1 if
|
||||
// the chaser died in the attempt.
|
||||
func (g *RogueGame) doChase(th *Monster) int {
|
||||
p := &g.Player
|
||||
stoprun := false // true means we are there
|
||||
mindist := 32767
|
||||
|
||||
rer := th.Room // find room of chaser
|
||||
if th.On(IsGreed) && rer.GoldVal == 0 {
|
||||
th.Dest = &p.Pos // if gold has been taken, run after hero
|
||||
}
|
||||
var ree *Room // find room of chasee
|
||||
if th.Dest == &p.Pos {
|
||||
ree = p.Room
|
||||
} else {
|
||||
ree = g.roomin(*th.Dest)
|
||||
}
|
||||
// We don't count doors as inside rooms for this routine
|
||||
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
|
||||
var this Coord
|
||||
|
||||
over:
|
||||
// If the object of our desire is in a different room, and we are not
|
||||
// in a corridor, run to the door nearest to our goal.
|
||||
if rer != ree {
|
||||
for i := range rer.Exits {
|
||||
curdist := distCp(*th.Dest, rer.Exits[i])
|
||||
if curdist < mindist {
|
||||
this = rer.Exits[i]
|
||||
mindist = curdist
|
||||
}
|
||||
}
|
||||
if door {
|
||||
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPNum]
|
||||
door = false
|
||||
goto over
|
||||
}
|
||||
} else {
|
||||
this = *th.Dest
|
||||
// For dragons check and see if (a) the hero is on a straight line
|
||||
// from it, and (b) that it is within shooting distance, but
|
||||
// outside of striking range.
|
||||
if th.Type == 'D' && (th.Pos.Y == p.Pos.Y || th.Pos.X == p.Pos.X ||
|
||||
abs(th.Pos.Y-p.Pos.Y) == abs(th.Pos.X-p.Pos.X)) &&
|
||||
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
|
||||
!th.On(IsCanc) && g.rnd(dragonShot) == 0 {
|
||||
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
|
||||
g.Delta.X = sign(p.Pos.X - th.Pos.X)
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
g.fireBolt(th.Pos, &g.Delta, "flame")
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !th.On(IsTarget) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
// This now contains what we want to run to this time so we run to it.
|
||||
// If we hit it we either want to fight it or stop running
|
||||
if !g.chase(th, this) {
|
||||
if this == p.Pos {
|
||||
return g.attack(th)
|
||||
} else if this == *th.Dest {
|
||||
for _, obj := range g.Level.Objects {
|
||||
if th.Dest == &obj.Pos {
|
||||
detachObj(&g.Level.Objects, obj)
|
||||
attachObj(&th.Pack, obj)
|
||||
if th.Room.Flags.Has(IsGone) {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
|
||||
}
|
||||
th.Dest = g.findDest(th)
|
||||
break
|
||||
}
|
||||
}
|
||||
if th.Type != 'F' {
|
||||
stoprun = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if th.Type == 'F' {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
g.relocate(th, g.chRet)
|
||||
// And stop running if need be
|
||||
if stoprun && th.Pos == *th.Dest {
|
||||
th.Flags.Clear(IsRun)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// chase finds the spot for the chaser to move closer to the chasee
|
||||
// (chase.c chase). Returns true if we want to keep on chasing later, false
|
||||
// if we reach the goal. The chosen spot lands in g.chRet.
|
||||
func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
p := &g.Player
|
||||
er := tp.Pos
|
||||
plcnt := 1
|
||||
var curdist int
|
||||
|
||||
// If the thing is confused, let it move randomly. Invisible Stalkers
|
||||
// are slightly confused all of the time, and bats are quite confused
|
||||
// all the time
|
||||
if (tp.On(IsHuh) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) ||
|
||||
(tp.Type == 'B' && g.rnd(2) == 0) {
|
||||
// get a valid random move
|
||||
g.chRet = g.rndmove(&tp.Creature)
|
||||
curdist = distCp(g.chRet, ee)
|
||||
// Small chance that it will become un-confused
|
||||
if g.rnd(20) == 0 {
|
||||
tp.Flags.Clear(IsHuh)
|
||||
}
|
||||
} else {
|
||||
// Otherwise, find the empty spot next to the chaser that is
|
||||
// closest to the chasee. This will eventually hold where we move
|
||||
// to get closer. If we can't find an empty spot, we stay where we
|
||||
// are.
|
||||
curdist = distCp(er, ee)
|
||||
g.chRet = er
|
||||
|
||||
ey := er.Y + 1
|
||||
if ey >= NumLines-1 {
|
||||
ey = NumLines - 2
|
||||
}
|
||||
ex := er.X + 1
|
||||
if ex >= NumCols {
|
||||
ex = NumCols - 1
|
||||
}
|
||||
|
||||
for x := er.X - 1; x <= ex; x++ {
|
||||
if x < 0 {
|
||||
continue
|
||||
}
|
||||
for y := er.Y - 1; y <= ey; y++ {
|
||||
tryp := Coord{X: x, Y: y}
|
||||
if !g.diagOk(er, tryp) {
|
||||
continue
|
||||
}
|
||||
ch := g.Level.VisibleChar(y, x)
|
||||
if stepOk(ch) {
|
||||
// If it is a scroll, it might be a scare monster
|
||||
// scroll so we need to look it up to see what type
|
||||
// it is.
|
||||
if ch == Scroll {
|
||||
var found *Object
|
||||
for _, obj := range g.Level.Objects {
|
||||
if y == obj.Pos.Y && x == obj.Pos.X {
|
||||
found = obj
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != nil && found.Which == SScare {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// It can also be a Xeroc, which we shouldn't step on
|
||||
if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' {
|
||||
continue
|
||||
}
|
||||
// If we didn't find any scrolls at this place or it
|
||||
// wasn't a scare scroll, then this place counts
|
||||
thisdist := distance(y, x, ee.Y, ee.X)
|
||||
if thisdist < curdist {
|
||||
plcnt = 1
|
||||
g.chRet = tryp
|
||||
curdist = thisdist
|
||||
} else if thisdist == curdist {
|
||||
if plcnt++; g.rnd(plcnt) == 0 {
|
||||
g.chRet = tryp
|
||||
curdist = thisdist
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return curdist != 0 && g.chRet != p.Pos
|
||||
}
|
||||
|
||||
// setOldch sets the oldch character for the monster (chase.c set_oldch).
|
||||
func (g *RogueGame) setOldch(tp *Monster, cp Coord) {
|
||||
@@ -113,7 +370,7 @@ func (g *RogueGame) cansee(y, x int) bool {
|
||||
// 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
|
||||
prob := g.Monsters[tp.Type-'A'].Carry
|
||||
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
|
||||
return &g.Player.Pos
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ func (g *RogueGame) runDaemon(id DaemonID, arg int) {
|
||||
g.rollwand(arg)
|
||||
case DDoctor:
|
||||
g.doctor(arg)
|
||||
case DStomach:
|
||||
g.stomach(arg)
|
||||
case DRunners:
|
||||
g.runners(arg)
|
||||
case DSwander:
|
||||
g.swander(arg)
|
||||
case DNohaste:
|
||||
@@ -120,6 +124,57 @@ func (g *RogueGame) nohaste(int) {
|
||||
g.msg("you feel yourself slowing down")
|
||||
}
|
||||
|
||||
// stomach digests the hero's food (daemons.c stomach).
|
||||
func (g *RogueGame) stomach(int) {
|
||||
p := &g.Player
|
||||
origHungry := p.HungryState
|
||||
if p.FoodLeft <= 0 {
|
||||
if p.FoodLeft--; p.FoodLeft < -StarveTime {
|
||||
g.death('s')
|
||||
}
|
||||
// the hero is fainting
|
||||
if g.NoCommand != 0 || g.rnd(5) != 0 {
|
||||
return
|
||||
}
|
||||
g.NoCommand += g.rnd(8) + 4
|
||||
p.HungryState = 3
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("%s", g.chooseStr(
|
||||
"the munchies overpower your motor capabilities. ",
|
||||
"you feel too weak from lack of food. "))
|
||||
}
|
||||
g.msg("%s", g.chooseStr("You freak out", "You faint"))
|
||||
} else {
|
||||
oldfood := p.FoodLeft
|
||||
amulet := 0
|
||||
if g.HasAmulet {
|
||||
amulet = 1
|
||||
}
|
||||
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
|
||||
|
||||
if p.FoodLeft < MoreTime && oldfood >= MoreTime {
|
||||
p.HungryState = 2
|
||||
g.msg("%s", g.chooseStr(
|
||||
"the munchies are interfering with your motor capabilites",
|
||||
"you are starting to feel weak"))
|
||||
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
|
||||
p.HungryState = 1
|
||||
if g.Options.Terse {
|
||||
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
|
||||
} else {
|
||||
g.msg("%s", g.chooseStr("you are getting the munchies",
|
||||
"you are starting to get hungry"))
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.HungryState != origHungry {
|
||||
p.Flags.Clear(IsRun)
|
||||
g.Running = false
|
||||
g.ToDeath = false
|
||||
g.Count = 0
|
||||
}
|
||||
}
|
||||
|
||||
// comeDown takes the hero down off her acid trip (daemons.c come_down).
|
||||
func (g *RogueGame) comeDown(int) {
|
||||
p := &g.Player
|
||||
|
||||
558
game/fight.go
558
game/fight.go
@@ -1,7 +1,49 @@
|
||||
package game
|
||||
|
||||
// fight.c — combat. setMname arrives first (monster wake-ups need it); the
|
||||
// combat resolution functions come with the combat phase.
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fight.c — all the fighting gets done here.
|
||||
|
||||
// hNames are the strings for hitting; the first four are used when the
|
||||
// player strikes, the second four for monsters (fight.c h_names).
|
||||
var hNames = [8]string{
|
||||
" scored an excellent hit on ",
|
||||
" hit ",
|
||||
" have injured ",
|
||||
" swing and hit ",
|
||||
" scored an excellent hit on ",
|
||||
" hit ",
|
||||
" has injured ",
|
||||
" swings and hits ",
|
||||
}
|
||||
|
||||
// mNames are the strings for missing (fight.c m_names).
|
||||
var mNames = [8]string{
|
||||
" miss",
|
||||
" swing and miss",
|
||||
" barely miss",
|
||||
" don't hit",
|
||||
" misses",
|
||||
" swings and misses",
|
||||
" barely misses",
|
||||
" doesn't hit",
|
||||
}
|
||||
|
||||
// strPlus adjusts hit probabilities due to strength (fight.c str_plus).
|
||||
var strPlus = [32]int{
|
||||
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
|
||||
}
|
||||
|
||||
// addDam adjusts damage done due to strength (fight.c add_dam).
|
||||
var addDam = [32]int{
|
||||
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
|
||||
3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
|
||||
}
|
||||
|
||||
// setMname returns the monster name for the given monster (fight.c
|
||||
// set_mname).
|
||||
@@ -20,9 +62,517 @@ func (g *RogueGame) setMname(tp *Monster) string {
|
||||
} else {
|
||||
ch -= 'A'
|
||||
}
|
||||
mname = monsterTable[ch].Name
|
||||
mname = g.Monsters[ch].Name
|
||||
} else {
|
||||
mname = monsterTable[tp.Type-'A'].Name
|
||||
mname = g.Monsters[tp.Type-'A'].Name
|
||||
}
|
||||
return "the " + mname
|
||||
}
|
||||
|
||||
// fight has the player attack the monster at mp (fight.c fight).
|
||||
func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
|
||||
p := &g.Player
|
||||
// Find the monster we want to fight
|
||||
tp := g.Level.MonsterAt(mp.Y, mp.X)
|
||||
if tp == nil {
|
||||
return false
|
||||
}
|
||||
// Since we are fighting, things are not quiet so no healing takes
|
||||
// place.
|
||||
g.Count = 0
|
||||
g.Quiet = 0
|
||||
g.runto(mp)
|
||||
// Let him know it was really a xeroc (if it was one).
|
||||
if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(IsBlind) {
|
||||
tp.Disguise = 'X'
|
||||
if p.On(IsHalu) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, byte(g.rnd(26)+'A'))
|
||||
}
|
||||
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
|
||||
"wait! That's a xeroc!"))
|
||||
if !thrown {
|
||||
return false
|
||||
}
|
||||
}
|
||||
mname := g.setMname(tp)
|
||||
didHit := false
|
||||
g.HasHit = g.Options.Terse && !g.ToDeath
|
||||
if g.rollEm(&p.Creature, &tp.Creature, weap, thrown) {
|
||||
didHit = false
|
||||
if thrown {
|
||||
g.thunk(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.hit("", mname, g.Options.Terse)
|
||||
}
|
||||
if p.On(CanHuh) {
|
||||
didHit = true
|
||||
tp.Flags.Set(IsHuh)
|
||||
p.Flags.Clear(CanHuh)
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
g.msg("your hands stop glowing %s", g.pickColor("red"))
|
||||
}
|
||||
if tp.Stats.HP <= 0 {
|
||||
g.killed(tp, true)
|
||||
} else if didHit && !p.On(IsBlind) {
|
||||
g.msg("%s appears confused", mname)
|
||||
}
|
||||
didHit = true
|
||||
} else {
|
||||
if thrown {
|
||||
g.bounce(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.miss("", mname, g.Options.Terse)
|
||||
}
|
||||
}
|
||||
return didHit
|
||||
}
|
||||
|
||||
// attack has the monster attack the player (fight.c attack). Returns -1 if
|
||||
// the monster removed itself from the level during its own attack.
|
||||
func (g *RogueGame) attack(mp *Monster) int {
|
||||
p := &g.Player
|
||||
// Since this is an attack, stop running and any healing that was
|
||||
// going on at the time.
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !mp.On(IsTarget) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(IsBlind) {
|
||||
mp.Disguise = 'X'
|
||||
if p.On(IsHalu) {
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, byte(g.rnd(26)+'A'))
|
||||
}
|
||||
}
|
||||
mname := g.setMname(mp)
|
||||
oldhp := p.Stats.HP
|
||||
removed := false
|
||||
if g.rollEm(&mp.Creature, &p.Creature, nil, false) {
|
||||
if mp.Type != 'I' {
|
||||
if g.HasHit {
|
||||
g.addmsg(". ")
|
||||
}
|
||||
g.hit(mname, "", false)
|
||||
} else if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
g.HasHit = false
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
} else if !g.Kamikaze {
|
||||
oldhp -= p.Stats.HP
|
||||
if oldhp > g.MaxHit {
|
||||
g.MaxHit = oldhp
|
||||
}
|
||||
if p.Stats.HP <= g.MaxHit {
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
if !mp.On(IsCanc) {
|
||||
switch mp.Type {
|
||||
case 'A':
|
||||
// If an aquator hits, you can lose armor class.
|
||||
g.rustArmor(p.CurArmor)
|
||||
case 'I':
|
||||
// The ice monster freezes you
|
||||
p.Flags.Clear(IsRun)
|
||||
if g.NoCommand == 0 {
|
||||
g.addmsg("you are frozen")
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" by the %s", mname)
|
||||
}
|
||||
g.endmsg()
|
||||
}
|
||||
g.NoCommand += g.rnd(2) + 2
|
||||
if g.NoCommand > BoreLevel {
|
||||
g.death('h')
|
||||
}
|
||||
case 'R':
|
||||
// Rattlesnakes have poisonous bites
|
||||
if !g.save(VsPoison) {
|
||||
if !p.IsWearing(RSustStr) {
|
||||
g.chgStr(-1)
|
||||
if !g.Options.Terse {
|
||||
g.msg("you feel a bite in your leg and now feel weaker")
|
||||
} else {
|
||||
g.msg("a bite has weakened you")
|
||||
}
|
||||
} else if !g.ToDeath {
|
||||
if !g.Options.Terse {
|
||||
g.msg("a bite momentarily weakens you")
|
||||
} else {
|
||||
g.msg("bite has no effect")
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'W', 'V':
|
||||
// Wraiths might drain energy levels, and Vampires can
|
||||
// steal max_hp
|
||||
chance := 30
|
||||
if mp.Type == 'W' {
|
||||
chance = 15
|
||||
}
|
||||
if g.rnd(100) < chance {
|
||||
var fewer int
|
||||
if mp.Type == 'W' {
|
||||
if p.Stats.Exp == 0 {
|
||||
g.death('W') // All levels gone
|
||||
}
|
||||
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
|
||||
p.Stats.Exp = 0
|
||||
p.Stats.Lvl = 1
|
||||
} else {
|
||||
p.Stats.Exp = eLevels[p.Stats.Lvl-1] + 1
|
||||
}
|
||||
fewer = g.roll(1, 10)
|
||||
} else {
|
||||
fewer = g.roll(1, 3)
|
||||
}
|
||||
p.Stats.HP -= fewer
|
||||
p.Stats.MaxHP -= fewer
|
||||
if p.Stats.HP <= 0 {
|
||||
p.Stats.HP = 1
|
||||
}
|
||||
if p.Stats.MaxHP <= 0 {
|
||||
g.death(mp.Type)
|
||||
}
|
||||
g.msg("you suddenly feel weaker")
|
||||
}
|
||||
case 'F':
|
||||
// Venus Flytrap stops the poor guy from moving
|
||||
p.Flags.Set(IsHeld)
|
||||
p.VfHit++
|
||||
g.Monsters['F'-'A'].Stats.Dmg = fmt.Sprintf("%dx1", p.VfHit)
|
||||
if p.Stats.HP--; p.Stats.HP <= 0 {
|
||||
g.death('F')
|
||||
}
|
||||
case 'L':
|
||||
// Leprechaun steals some gold
|
||||
lastpurse := p.Purse
|
||||
p.Purse -= g.goldCalc()
|
||||
if !g.save(VsMagic) {
|
||||
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
if p.Purse < 0 {
|
||||
p.Purse = 0
|
||||
}
|
||||
g.removeMon(mp.Pos, mp, false)
|
||||
removed = true
|
||||
if p.Purse != lastpurse {
|
||||
g.msg("your purse feels lighter")
|
||||
}
|
||||
case 'N':
|
||||
// Nymphs steal a magic item; look through the pack and
|
||||
// pick out one we like.
|
||||
var steal *Object
|
||||
nobj := 0
|
||||
for _, obj := range p.Pack {
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
|
||||
obj.isMagic() {
|
||||
if nobj++; g.rnd(nobj) == 0 {
|
||||
steal = obj
|
||||
}
|
||||
}
|
||||
}
|
||||
if steal != nil {
|
||||
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
|
||||
removed = true
|
||||
g.leavePack(steal, false, false)
|
||||
g.msg("she stole %s!", g.invName(steal, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if mp.Type != 'I' {
|
||||
if g.HasHit {
|
||||
g.addmsg(". ")
|
||||
g.HasHit = false
|
||||
}
|
||||
if mp.Type == 'F' {
|
||||
p.Stats.HP -= p.VfHit
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
}
|
||||
}
|
||||
g.miss(mname, "", false)
|
||||
}
|
||||
if g.Options.FightFlush && !g.ToDeath {
|
||||
g.flushType()
|
||||
}
|
||||
g.Count = 0
|
||||
g.status()
|
||||
if removed {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// swing returns true if the swing hits (fight.c swing).
|
||||
func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
|
||||
res := g.rnd(20)
|
||||
need := (20 - atLvl) - opArm
|
||||
return res+wplus >= need
|
||||
}
|
||||
|
||||
// rollEm rolls several attacks (fight.c roll_em).
|
||||
func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool {
|
||||
p := &g.Player
|
||||
att := &thatt.Stats
|
||||
def := &thdef.Stats
|
||||
var cp string
|
||||
var hplus, dplus int
|
||||
if weap == nil {
|
||||
cp = att.Dmg
|
||||
} else {
|
||||
hplus = weap.HPlus
|
||||
dplus = weap.DPlus
|
||||
if weap == p.CurWeapon {
|
||||
if p.IsRing(Left, RAddDam) {
|
||||
dplus += p.CurRing[Left].Arm
|
||||
} else if p.IsRing(Left, RAddHit) {
|
||||
hplus += p.CurRing[Left].Arm
|
||||
}
|
||||
if p.IsRing(Right, RAddDam) {
|
||||
dplus += p.CurRing[Right].Arm
|
||||
} else if p.IsRing(Right, RAddHit) {
|
||||
hplus += p.CurRing[Right].Arm
|
||||
}
|
||||
}
|
||||
cp = weap.Damage
|
||||
if hurl {
|
||||
if weap.Flags.Has(IsMissl) && p.CurWeapon != nil &&
|
||||
p.CurWeapon.Which == weap.Launch {
|
||||
cp = weap.HurlDmg
|
||||
hplus += p.CurWeapon.HPlus
|
||||
dplus += p.CurWeapon.DPlus
|
||||
} else if weap.Launch < 0 {
|
||||
cp = weap.HurlDmg
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the creature being attacked is not running (asleep or held) then
|
||||
// the attacker gets a plus four bonus to hit.
|
||||
if !thdef.Flags.Has(IsRun) {
|
||||
hplus += 4
|
||||
}
|
||||
defArm := def.Arm
|
||||
if def == &p.Stats {
|
||||
if p.CurArmor != nil {
|
||||
defArm = p.CurArmor.Arm
|
||||
}
|
||||
if p.IsRing(Left, RProtect) {
|
||||
defArm -= p.CurRing[Left].Arm
|
||||
}
|
||||
if p.IsRing(Right, RProtect) {
|
||||
defArm -= p.CurRing[Right].Arm
|
||||
}
|
||||
}
|
||||
didHit := false
|
||||
for cp != "" {
|
||||
ndice := cAtoi(cp)
|
||||
xi := strings.IndexByte(cp, 'x')
|
||||
if xi < 0 {
|
||||
break
|
||||
}
|
||||
cp = cp[xi+1:]
|
||||
nsides := cAtoi(cp)
|
||||
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) {
|
||||
proll := g.roll(ndice, nsides)
|
||||
damage := dplus + proll + addDam[att.Str]
|
||||
if damage > 0 {
|
||||
def.HP -= damage
|
||||
}
|
||||
didHit = true
|
||||
}
|
||||
si := strings.IndexByte(cp, '/')
|
||||
if si < 0 {
|
||||
break
|
||||
}
|
||||
cp = cp[si+1:]
|
||||
}
|
||||
return didHit
|
||||
}
|
||||
|
||||
// cAtoi parses a leading integer like C atoi: trailing non-digits are
|
||||
// ignored rather than an error.
|
||||
func cAtoi(s string) int {
|
||||
i := 0
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
n, _ := strconv.Atoi(s[:i])
|
||||
return n
|
||||
}
|
||||
|
||||
// prname gives the print name of a combatant; "" is the player (fight.c
|
||||
// prname).
|
||||
func prname(mname string, upper bool) string {
|
||||
out := mname
|
||||
if out == "" {
|
||||
out = "you"
|
||||
}
|
||||
if upper {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// thunk announces that a missile hit a monster (fight.c thunk).
|
||||
func (g *RogueGame) thunk(weap *Object, mname string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
if weap.Type == Weapon {
|
||||
g.addmsg("the %s hits ", g.Items.Weapons[weap.Which].Name)
|
||||
} else {
|
||||
g.addmsg("you hit ")
|
||||
}
|
||||
g.addmsg("%s", mname)
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// hit prints a message to indicate a successful hit (fight.c hit).
|
||||
func (g *RogueGame) hit(er, ee string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
g.addmsg("%s", prname(er, true))
|
||||
var s string
|
||||
if g.Options.Terse {
|
||||
s = " hit"
|
||||
} else {
|
||||
i := g.rnd(4)
|
||||
if er != "" {
|
||||
i += 4
|
||||
}
|
||||
s = hNames[i]
|
||||
}
|
||||
g.addmsg("%s", s)
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("%s", prname(ee, false))
|
||||
}
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// miss prints a message to indicate a poor swing (fight.c miss).
|
||||
func (g *RogueGame) miss(er, ee string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
g.addmsg("%s", prname(er, true))
|
||||
i := 0
|
||||
if !g.Options.Terse {
|
||||
i = g.rnd(4)
|
||||
}
|
||||
if er != "" {
|
||||
i += 4
|
||||
}
|
||||
g.addmsg("%s", mNames[i])
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" %s", prname(ee, false))
|
||||
}
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// bounce announces that a missile missed a monster (fight.c bounce).
|
||||
func (g *RogueGame) bounce(weap *Object, mname string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
if weap.Type == Weapon {
|
||||
g.addmsg("the %s misses ", g.Items.Weapons[weap.Which].Name)
|
||||
} else {
|
||||
g.addmsg("you missed ")
|
||||
}
|
||||
g.addmsg("%s", mname)
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// removeMon removes a monster from the screen (fight.c remove_mon).
|
||||
func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
|
||||
pack := append([]*Object(nil), tp.Pack...)
|
||||
for _, obj := range pack {
|
||||
obj.Pos = tp.Pos
|
||||
detachObj(&tp.Pack, obj)
|
||||
if waskill {
|
||||
g.fall(obj, false)
|
||||
}
|
||||
}
|
||||
g.Level.SetMonsterAt(mp.Y, mp.X, nil)
|
||||
g.mvaddch(mp.Y, mp.X, tp.OldCh)
|
||||
detachMon(&g.Level.Monsters, tp)
|
||||
if tp.On(IsTarget) {
|
||||
g.Kamikaze = false
|
||||
g.ToDeath = false
|
||||
if g.Options.FightFlush {
|
||||
g.flushType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// killed is called to put a monster to death (fight.c killed).
|
||||
func (g *RogueGame) killed(tp *Monster, pr bool) {
|
||||
p := &g.Player
|
||||
p.Stats.Exp += tp.Stats.Exp
|
||||
|
||||
// If the monster was a venus flytrap, un-hold him
|
||||
switch tp.Type {
|
||||
case 'F':
|
||||
p.Flags.Clear(IsHeld)
|
||||
p.VfHit = 0
|
||||
g.Monsters['F'-'A'].Stats.Dmg = "000x0"
|
||||
case 'L':
|
||||
pos, ok := g.fallpos(tp.Pos)
|
||||
if ok {
|
||||
tp.Room.Gold = pos
|
||||
}
|
||||
if ok && g.Depth >= g.MaxDepth {
|
||||
gold := newObject()
|
||||
gold.Type = Gold
|
||||
gold.SetGoldVal(g.goldCalc())
|
||||
if g.save(VsMagic) {
|
||||
gold.Arm += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
attachObj(&tp.Pack, gold)
|
||||
}
|
||||
}
|
||||
// Get rid of the monster.
|
||||
mname := g.setMname(tp)
|
||||
g.removeMon(tp.Pos, tp, true)
|
||||
if pr {
|
||||
if g.HasHit {
|
||||
g.addmsg(". Defeated ")
|
||||
g.HasHit = false
|
||||
} else {
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you have ")
|
||||
}
|
||||
g.addmsg("defeated ")
|
||||
}
|
||||
g.msg("%s", mname)
|
||||
}
|
||||
// Do adjustments if he went up a level
|
||||
g.checkLevel()
|
||||
if g.Options.FightFlush {
|
||||
g.flushType()
|
||||
}
|
||||
}
|
||||
|
||||
// flushType flushes typeahead for the fight_flush option (mach_dep.c
|
||||
// flush_type / curses flushinp).
|
||||
func (g *RogueGame) flushType() {
|
||||
if f, ok := g.scr.term.(interface{ FlushInput() }); ok {
|
||||
f.FlushInput()
|
||||
}
|
||||
}
|
||||
|
||||
118
game/fight_test.go
Normal file
118
game/fight_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// mkGame builds a headless game on a generated level, initializing the
|
||||
// look() state the way playit() does before the first command.
|
||||
func mkGame(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
g := NewGame(Config{Seed: seed, Term: &testTerm{}})
|
||||
g.NewLevel()
|
||||
g.Oldpos = g.Player.Pos
|
||||
g.Oldrp = g.roomin(g.Player.Pos)
|
||||
return g
|
||||
}
|
||||
|
||||
// spawnAdjacent puts a monster of the given type next to the hero.
|
||||
func spawnAdjacent(g *RogueGame, typ byte) *Monster {
|
||||
pos := Coord{X: g.Player.Pos.X + 1, Y: g.Player.Pos.Y}
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, typ, pos)
|
||||
return tp
|
||||
}
|
||||
|
||||
func TestRollEmParsesMultiAttackDice(t *testing.T) {
|
||||
g := mkGame(t, 42)
|
||||
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: "1x4/1x4/1x4"}}
|
||||
def := &Creature{Stats: Stats{Arm: 10, HP: 1000}}
|
||||
def.Flags.Set(IsRun)
|
||||
// With attacker level 20 vs armor 10, swing always hits
|
||||
// (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of
|
||||
// 1x4 + str bonus 1 each must deal between 6 and 15 damage.
|
||||
if !g.rollEm(att, def, nil, false) {
|
||||
t.Fatal("attack with guaranteed swing missed")
|
||||
}
|
||||
dmg := 1000 - def.Stats.HP
|
||||
if dmg < 6 || dmg > 15 {
|
||||
t.Errorf("three 1x4+1 attacks dealt %d damage, want 6..15", dmg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFightKillsMonster(t *testing.T) {
|
||||
g := mkGame(t, 7)
|
||||
tp := spawnAdjacent(g, 'B') // bat: 1 hit die
|
||||
tp.Stats.HP = 1
|
||||
g.Player.Stats.Lvl = 20 // always hits
|
||||
before := len(g.Level.Monsters)
|
||||
g.fight(tp.Pos, g.Player.CurWeapon, false)
|
||||
if len(g.Level.Monsters) != before-1 {
|
||||
t.Error("monster not removed after fatal fight")
|
||||
}
|
||||
if g.Level.MonsterAt(tp.Pos.Y, tp.Pos.X) != nil {
|
||||
t.Error("map still records dead monster")
|
||||
}
|
||||
if g.Player.Stats.Exp == 0 {
|
||||
t.Error("no experience for the kill")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttackHurtsPlayer(t *testing.T) {
|
||||
g := mkGame(t, 9)
|
||||
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
|
||||
tp.Stats.Lvl = 20 // always hits
|
||||
tp.Flags.Clear(IsCanc)
|
||||
hpBefore := g.Player.Stats.HP
|
||||
g.Player.Stats.HP = 500
|
||||
g.Player.Stats.MaxHP = 500
|
||||
g.attack(tp)
|
||||
if g.Player.Stats.HP >= 500 {
|
||||
t.Errorf("player HP unchanged (%d -> %d)", hpBefore, g.Player.Stats.HP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeathUnwindsWithGameEnd(t *testing.T) {
|
||||
g := mkGame(t, 11)
|
||||
defer func() {
|
||||
r := recover()
|
||||
if _, ok := r.(gameEnd); !ok {
|
||||
t.Fatalf("death did not unwind with gameEnd, got %v", r)
|
||||
}
|
||||
if g.Playing {
|
||||
t.Error("still playing after death")
|
||||
}
|
||||
}()
|
||||
g.Options.Tombstone = false
|
||||
g.death('K')
|
||||
}
|
||||
|
||||
func TestRunnersChaseHero(t *testing.T) {
|
||||
g := mkGame(t, 3)
|
||||
// Place a hobgoblin a few squares away in the hero's room and set it
|
||||
// running at the hero.
|
||||
p := &g.Player
|
||||
pos := Coord{X: p.Pos.X + 3, Y: p.Pos.Y}
|
||||
if !stepOk(g.Level.Char(pos.Y, pos.X)) || g.Level.MonsterAt(pos.Y, pos.X) != nil {
|
||||
t.Skip("no clear lane on this seed")
|
||||
}
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, 'H', pos)
|
||||
tp.Flags.Set(IsRun)
|
||||
tp.Dest = &p.Pos
|
||||
d0 := distCp(tp.Pos, p.Pos)
|
||||
g.runners(0)
|
||||
if d1 := distCp(tp.Pos, p.Pos); d1 >= d0 {
|
||||
t.Errorf("monster did not close distance: %d -> %d", d0, d1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKilledLeprechaunDropsGoldViaFall(t *testing.T) {
|
||||
g := mkGame(t, 21)
|
||||
tp := spawnAdjacent(g, 'L')
|
||||
tp.Stats.HP = 0
|
||||
objsBefore := len(g.Level.Objects)
|
||||
g.killed(tp, false)
|
||||
// At max depth the leprechaun's hoard falls to the floor.
|
||||
if len(g.Level.Objects) <= objsBefore-1 {
|
||||
t.Error("level object list shrank unexpectedly")
|
||||
}
|
||||
}
|
||||
16
game/game.go
16
game/game.go
@@ -99,6 +99,7 @@ type RogueGame struct {
|
||||
maze mazeState // rooms.c maze statics
|
||||
pnum int // passages.c passnum statics
|
||||
newpnum bool
|
||||
chRet Coord // chase.c static ch_ret: where chasing takes you
|
||||
|
||||
// daemons/fuses
|
||||
Daemons DaemonList
|
||||
@@ -121,6 +122,10 @@ type RogueGame struct {
|
||||
// options and item identity
|
||||
Options Options
|
||||
Items ItemLore
|
||||
// Monsters is the per-game copy of the bestiary: the C code mutates
|
||||
// the venus flytrap's damage string during play, and the table is
|
||||
// part of the save state.
|
||||
Monsters [26]MonsterKind
|
||||
|
||||
// scores
|
||||
LastScore int
|
||||
@@ -159,6 +164,7 @@ func NewGame(cfg Config) *RogueGame {
|
||||
}
|
||||
// TODO(port): parse cfg.RogueOpts once options.go lands.
|
||||
|
||||
g.Monsters = monsterTable
|
||||
g.Items.Group = 2 // weapons.c: int group = 2
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Flags = IsGone | IsDark
|
||||
@@ -187,8 +193,8 @@ func (g *RogueGame) quit(int) {
|
||||
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()
|
||||
g.score(g.Player.Purse, 1, 0)
|
||||
g.myExit(0)
|
||||
return
|
||||
}
|
||||
g.move(0, 0)
|
||||
@@ -200,9 +206,3 @@ func (g *RogueGame) quit(int) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
|
||||
tp.OldCh = g.inch()
|
||||
tp.Room = g.roomin(cp)
|
||||
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
|
||||
mp := &monsterTable[tp.Type-'A']
|
||||
mp := &g.Monsters[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
|
||||
@@ -165,7 +165,7 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
|
||||
// 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 {
|
||||
if g.Depth >= g.MaxDepth && g.rnd(100) < g.Monsters[tp.Type-'A'].Carry {
|
||||
attachObj(&tp.Pack, g.newThing())
|
||||
}
|
||||
}
|
||||
|
||||
326
game/move.go
326
game/move.go
@@ -1,7 +1,180 @@
|
||||
package game
|
||||
|
||||
// move.c — hero movement. doorOpen arrives first (room entry needs it);
|
||||
// the movement commands and traps come with the UI phase.
|
||||
// move.c — hero movement commands.
|
||||
|
||||
// doRun starts the hero running (move.c do_run).
|
||||
func (g *RogueGame) doRun(ch byte) {
|
||||
g.Running = true
|
||||
g.After = false
|
||||
g.RunCh = ch
|
||||
}
|
||||
|
||||
// doMove checks that a move is legal and handles the consequences —
|
||||
// fighting, picking up, etc. (move.c do_move).
|
||||
func (g *RogueGame) doMove(dy, dx int) {
|
||||
p := &g.Player
|
||||
g.Firstmove = false
|
||||
if g.NoMove > 0 {
|
||||
g.NoMove--
|
||||
g.msg("you are still stuck in the bear trap")
|
||||
return
|
||||
}
|
||||
// Do a confused move (maybe)
|
||||
var nh Coord
|
||||
if p.On(IsHuh) && g.rnd(5) != 0 {
|
||||
nh = g.rndmove(&p.Creature)
|
||||
if nh == p.Pos {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
g.ToDeath = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
}
|
||||
|
||||
over:
|
||||
// Check if he tried to move off the screen or make an illegal diagonal
|
||||
// move, and stop him if he did.
|
||||
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
|
||||
var ch byte
|
||||
var fl PlaceFlags
|
||||
if !hitBound {
|
||||
if !g.diagOk(p.Pos, nh) {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
return
|
||||
}
|
||||
if g.Running && p.Pos == nh {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
}
|
||||
fl = *g.Level.FlagsAt(nh.Y, nh.X)
|
||||
ch = g.Level.VisibleChar(nh.Y, nh.X)
|
||||
if !fl.Has(FReal) && ch == Floor {
|
||||
if !p.On(IsLevit) {
|
||||
ch = Trap
|
||||
g.Level.SetChar(nh.Y, nh.X, Trap)
|
||||
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
|
||||
}
|
||||
} else if p.On(IsHeld) && ch != 'F' {
|
||||
g.msg("you are being held")
|
||||
return
|
||||
}
|
||||
}
|
||||
if hitBound {
|
||||
ch = ' ' // fall into the wall case below
|
||||
}
|
||||
switch ch {
|
||||
case ' ', '|', '-':
|
||||
if g.Options.PassGo && g.Running && p.Room.Flags.Has(IsGone) &&
|
||||
!p.On(IsBlind) {
|
||||
var b1, b2 bool
|
||||
switch g.RunCh {
|
||||
case 'h', 'l':
|
||||
b1 = p.Pos.Y != 1 && g.turnOk(p.Pos.Y-1, p.Pos.X)
|
||||
b2 = p.Pos.Y != NumLines-2 && g.turnOk(p.Pos.Y+1, p.Pos.X)
|
||||
if b1 != b2 {
|
||||
if b1 {
|
||||
g.RunCh = 'k'
|
||||
dy = -1
|
||||
} else {
|
||||
g.RunCh = 'j'
|
||||
dy = 1
|
||||
}
|
||||
dx = 0
|
||||
g.turnref()
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
goto over
|
||||
}
|
||||
case 'j', 'k':
|
||||
b1 = p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1)
|
||||
b2 = p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
|
||||
if b1 != b2 {
|
||||
if b1 {
|
||||
g.RunCh = 'h'
|
||||
dx = -1
|
||||
} else {
|
||||
g.RunCh = 'l'
|
||||
dx = 1
|
||||
}
|
||||
dy = 0
|
||||
g.turnref()
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
goto over
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Running = false
|
||||
g.After = false
|
||||
case Door:
|
||||
g.Running = false
|
||||
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPass) {
|
||||
g.enterRoom(nh)
|
||||
}
|
||||
g.moveStuff(nh, fl)
|
||||
case Trap:
|
||||
tr := g.beTrapped(nh)
|
||||
if tr == TDoor || tr == TTelep {
|
||||
return
|
||||
}
|
||||
g.moveStuff(nh, fl)
|
||||
case Passage:
|
||||
// when you're in a corridor, you don't know if you're in a maze
|
||||
// room or not, and there ain't no way to find out if you're
|
||||
// leaving a maze room, so it is necessary to always recalculate
|
||||
// proom.
|
||||
p.Room = g.roomin(p.Pos)
|
||||
g.moveStuff(nh, fl)
|
||||
case Floor:
|
||||
if !fl.Has(FReal) {
|
||||
g.beTrapped(p.Pos)
|
||||
}
|
||||
g.moveStuff(nh, fl)
|
||||
default:
|
||||
if ch == Stairs {
|
||||
g.SeenStairs = true
|
||||
}
|
||||
g.Running = false
|
||||
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
|
||||
g.fight(nh, p.CurWeapon, false)
|
||||
} else {
|
||||
if ch != Stairs {
|
||||
g.Take = ch
|
||||
}
|
||||
g.moveStuff(nh, fl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// moveStuff is the move_stuff label in do_move: complete the step.
|
||||
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
|
||||
p := &g.Player
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
||||
if fl.Has(FPass) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door {
|
||||
g.leaveRoom(nh)
|
||||
}
|
||||
p.Pos = nh
|
||||
}
|
||||
|
||||
// turnOk decides whether it is legal to turn onto the given space
|
||||
// (move.c turn_ok).
|
||||
func (g *RogueGame) turnOk(y, x int) bool {
|
||||
pp := g.Level.At(y, x)
|
||||
return pp.Ch == Door || pp.Flags&(FReal|FPass) == (FReal|FPass)
|
||||
}
|
||||
|
||||
// turnref decides whether to refresh at a passage turning (move.c turnref).
|
||||
func (g *RogueGame) turnref() {
|
||||
p := &g.Player
|
||||
pp := g.Level.At(p.Pos.Y, p.Pos.X)
|
||||
if !pp.Flags.Has(FSeen) {
|
||||
if g.Options.Jump {
|
||||
g.refresh()
|
||||
}
|
||||
pp.Flags.Set(FSeen)
|
||||
}
|
||||
}
|
||||
|
||||
// doorOpen is called to wake up things in a room that might move when the
|
||||
// hero enters (move.c door_open).
|
||||
@@ -17,3 +190,152 @@ func (g *RogueGame) doorOpen(rp *Room) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// beTrapped makes him pay for stepping on a trap (move.c be_trapped).
|
||||
func (g *RogueGame) beTrapped(tc Coord) int {
|
||||
p := &g.Player
|
||||
if p.On(IsLevit) {
|
||||
return TRust // anything that's not a door or teleport
|
||||
}
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
pp := g.Level.At(tc.Y, tc.X)
|
||||
pp.Ch = Trap
|
||||
tr := int(pp.Flags & FTMask)
|
||||
pp.Flags.Set(FSeen)
|
||||
switch tr {
|
||||
case TDoor:
|
||||
g.Depth++
|
||||
g.NewLevel()
|
||||
g.msg("you fell into a trap!")
|
||||
case TBear:
|
||||
g.NoMove += g.spread(3) // BEARTIME
|
||||
g.msg("you are caught in a bear trap")
|
||||
case TMyst:
|
||||
switch g.rnd(11) {
|
||||
case 0:
|
||||
g.msg("you are suddenly in a parallel dimension")
|
||||
case 1:
|
||||
g.msg("the light in here suddenly seems %s", rainbow[g.rnd(len(rainbow))])
|
||||
case 2:
|
||||
g.msg("you feel a sting in the side of your neck")
|
||||
case 3:
|
||||
g.msg("multi-colored lines swirl around you, then fade")
|
||||
case 4:
|
||||
g.msg("a %s light flashes in your eyes", rainbow[g.rnd(len(rainbow))])
|
||||
case 5:
|
||||
g.msg("a spike shoots past your ear!")
|
||||
case 6:
|
||||
g.msg("%s sparks dance across your armor", rainbow[g.rnd(len(rainbow))])
|
||||
case 7:
|
||||
g.msg("you suddenly feel very thirsty")
|
||||
case 8:
|
||||
g.msg("you feel time speed up suddenly")
|
||||
case 9:
|
||||
g.msg("time now seems to be going slower")
|
||||
case 10:
|
||||
g.msg("you pack turns %s!", rainbow[g.rnd(len(rainbow))])
|
||||
}
|
||||
case TSleep:
|
||||
g.NoCommand += g.spread(5) // SLEEPTIME
|
||||
p.Flags.Clear(IsRun)
|
||||
g.msg("a strange white mist envelops you and you fall asleep")
|
||||
case TArrow:
|
||||
if g.swing(p.Stats.Lvl-1, p.Stats.Arm, 1) {
|
||||
p.Stats.HP -= g.roll(1, 6)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("an arrow killed you")
|
||||
g.death('a')
|
||||
} else {
|
||||
g.msg("oh no! An arrow shot you")
|
||||
}
|
||||
} else {
|
||||
arrow := newObject()
|
||||
g.initWeapon(arrow, Arrow)
|
||||
arrow.Count = 1
|
||||
arrow.Pos = p.Pos
|
||||
g.fall(arrow, false)
|
||||
g.msg("an arrow shoots past you")
|
||||
}
|
||||
case TTelep:
|
||||
// since the hero's leaving, look() won't put a TRAP down for us,
|
||||
// so we have to do it ourself
|
||||
g.teleport()
|
||||
g.mvaddch(tc.Y, tc.X, Trap)
|
||||
case TDart:
|
||||
if !g.swing(p.Stats.Lvl+1, p.Stats.Arm, 1) {
|
||||
g.msg("a small dart whizzes by your ear and vanishes")
|
||||
} else {
|
||||
p.Stats.HP -= g.roll(1, 4)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("a poisoned dart killed you")
|
||||
g.death('d')
|
||||
}
|
||||
if !p.IsWearing(RSustStr) && !g.save(VsPoison) {
|
||||
g.chgStr(-1)
|
||||
}
|
||||
g.msg("a small dart just hit you in the shoulder")
|
||||
}
|
||||
case TRust:
|
||||
g.msg("a gush of water hits you on the head")
|
||||
g.rustArmor(p.CurArmor)
|
||||
}
|
||||
g.flushType()
|
||||
return tr
|
||||
}
|
||||
|
||||
// rndmove moves in a random direction if the monster/person is confused
|
||||
// (move.c rndmove).
|
||||
func (g *RogueGame) rndmove(who *Creature) Coord {
|
||||
ret := Coord{
|
||||
Y: who.Pos.Y + g.rnd(3) - 1,
|
||||
X: who.Pos.X + g.rnd(3) - 1,
|
||||
}
|
||||
// Now check to see if that's a legal move. If not, don't move.
|
||||
// (I.e., bump into the wall or whatever)
|
||||
if ret == who.Pos {
|
||||
return ret
|
||||
}
|
||||
if !g.diagOk(who.Pos, ret) {
|
||||
return who.Pos
|
||||
}
|
||||
ch := g.Level.VisibleChar(ret.Y, ret.X)
|
||||
if !stepOk(ch) {
|
||||
return who.Pos
|
||||
}
|
||||
if ch == Scroll {
|
||||
var found *Object
|
||||
for _, obj := range g.Level.Objects {
|
||||
if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X {
|
||||
found = obj
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != nil && found.Which == SScare {
|
||||
return who.Pos
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// rustArmor rusts the given armor, if it is a legal kind to rust, and we
|
||||
// aren't wearing a magic ring (move.c rust_armor).
|
||||
func (g *RogueGame) rustArmor(arm *Object) {
|
||||
if arm == nil || arm.Type != Armor || arm.Which == Leather ||
|
||||
arm.Arm >= 9 {
|
||||
return
|
||||
}
|
||||
|
||||
if arm.Flags.Has(IsProt) || g.Player.IsWearing(RSustArm) {
|
||||
if !g.ToDeath {
|
||||
g.msg("the rust vanishes instantly")
|
||||
}
|
||||
} else {
|
||||
arm.Arm++
|
||||
if !g.Options.Terse {
|
||||
g.msg("your armor appears to be weaker now. Oh my!")
|
||||
} else {
|
||||
g.msg("your armor weakens")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
229
game/rip.go
Normal file
229
game/rip.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// rip.c — the fun ends: death or a total win.
|
||||
//
|
||||
// The C functions here call exit(); the port panics with a gameEnd sentinel
|
||||
// that Run recovers, so the terminal is restored by normal unwinding.
|
||||
|
||||
// gameEnd is the sentinel carried by the panic that replaces my_exit().
|
||||
type gameEnd struct{ status int }
|
||||
|
||||
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
|
||||
func (g *RogueGame) myExit(st int) {
|
||||
g.Playing = false
|
||||
panic(gameEnd{status: st})
|
||||
}
|
||||
|
||||
var ripArt = []string{
|
||||
" __________",
|
||||
" / \\",
|
||||
" / REST \\",
|
||||
" / IN \\",
|
||||
" / PEACE \\",
|
||||
" / \\",
|
||||
" | |",
|
||||
" | |",
|
||||
" | killed by a |",
|
||||
" | |",
|
||||
" | 1980 |",
|
||||
" *| * * * | *",
|
||||
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
|
||||
}
|
||||
|
||||
// death does something really fun when he dies (rip.c death).
|
||||
func (g *RogueGame) death(monst byte) {
|
||||
p := &g.Player
|
||||
p.Purse -= p.Purse / 10
|
||||
g.clear()
|
||||
killer := g.killname(monst, false)
|
||||
if !g.Options.Tombstone {
|
||||
g.scr.Std.MvPrintw(NumLines-2, 0, "Killed by ")
|
||||
if monst != 's' && monst != 'h' {
|
||||
g.printw("a%s ", vowelstr(killer))
|
||||
}
|
||||
g.printw("%s with %d gold", killer, p.Purse)
|
||||
} else {
|
||||
year := time.Now().Year()
|
||||
for i, line := range ripArt {
|
||||
g.scr.Std.MvAddStr(8+i, 0, line)
|
||||
}
|
||||
g.mvaddstr(17, center(killer), killer)
|
||||
if monst == 's' || monst == 'h' {
|
||||
g.mvaddch(16, 32, ' ')
|
||||
} else {
|
||||
g.mvaddstr(16, 33, vowelstr(killer))
|
||||
}
|
||||
g.mvaddstr(14, center(g.Whoami), g.Whoami)
|
||||
au := fmt.Sprintf("%d Au", p.Purse)
|
||||
g.mvaddstr(15, center(au), au)
|
||||
g.mvaddstr(18, 26, fmt.Sprintf("%4d", year))
|
||||
}
|
||||
g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
|
||||
g.refresh()
|
||||
flags := 0
|
||||
if g.HasAmulet {
|
||||
flags = 3
|
||||
}
|
||||
g.score(p.Purse, flags, monst)
|
||||
g.waitFor('\n')
|
||||
g.myExit(0)
|
||||
}
|
||||
|
||||
// center returns the column to center the given string on the tombstone
|
||||
// (rip.c center).
|
||||
func center(str string) int {
|
||||
return 28 - (len(str)+1)/2
|
||||
}
|
||||
|
||||
// totalWinner is the code for a winner (rip.c total_winner).
|
||||
func (g *RogueGame) totalWinner() {
|
||||
p := &g.Player
|
||||
g.clear()
|
||||
g.standout()
|
||||
banner := []string{
|
||||
" ",
|
||||
" @ @ @ @ @ @@@ @ @ ",
|
||||
" @ @ @@ @@ @ @ @ @ ",
|
||||
" @ @ @@@ @ @ @ @ @ @@@ @@@@ @@@ @ @@@ @ ",
|
||||
" @@@@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ ",
|
||||
" @ @ @ @ @ @ @ @@@@ @ @ @@@@@ @ @ @ ",
|
||||
" @ @ @ @ @ @@ @ @ @ @ @ @ @ @ @ @ ",
|
||||
" @@@ @@@ @@ @ @ @ @@@@ @@@@ @@@ @@@ @@ @ ",
|
||||
" ",
|
||||
" Congratulations, you have made it to the light of day! ",
|
||||
}
|
||||
for i, line := range banner {
|
||||
g.scr.Std.MvAddStr(i, 0, line)
|
||||
}
|
||||
g.standend()
|
||||
g.scr.Std.MvAddStr(10, 0, "You have joined the elite ranks of those who have escaped the")
|
||||
g.scr.Std.MvAddStr(11, 0, "Dungeons of Doom alive. You journey home and sell all your loot at")
|
||||
g.scr.Std.MvAddStr(12, 0, "a great profit and are admitted to the Fighters' Guild.")
|
||||
g.mvaddstr(NumLines-1, 0, "--Press space to continue--")
|
||||
g.refresh()
|
||||
g.waitFor(' ')
|
||||
g.clear()
|
||||
g.mvaddstr(0, 0, " Worth Item")
|
||||
g.move(1, 0)
|
||||
oldpurse := p.Purse
|
||||
line := 1
|
||||
for _, obj := range p.Pack {
|
||||
worth := 0
|
||||
it := &g.Items
|
||||
switch obj.Type {
|
||||
case Food:
|
||||
worth = 2 * obj.Count
|
||||
case Weapon:
|
||||
worth = it.Weapons[obj.Which].Worth
|
||||
worth *= 3*(obj.HPlus+obj.DPlus) + obj.Count
|
||||
obj.Flags.Set(IsKnow)
|
||||
case Armor:
|
||||
worth = it.Armors[obj.Which].Worth
|
||||
worth += (9 - obj.Arm) * 100
|
||||
worth += 10 * (aClass[obj.Which] - obj.Arm)
|
||||
obj.Flags.Set(IsKnow)
|
||||
case Scroll:
|
||||
op := &it.Scrolls[obj.Which]
|
||||
worth = op.Worth * obj.Count
|
||||
if !op.Know {
|
||||
worth /= 2
|
||||
}
|
||||
op.Know = true
|
||||
case Potion:
|
||||
op := &it.Potions[obj.Which]
|
||||
worth = op.Worth * obj.Count
|
||||
if !op.Know {
|
||||
worth /= 2
|
||||
}
|
||||
op.Know = true
|
||||
case Ring:
|
||||
op := &it.Rings[obj.Which]
|
||||
worth = op.Worth
|
||||
if obj.Which == RAddStr || obj.Which == RAddDam ||
|
||||
obj.Which == RProtect || obj.Which == RAddHit {
|
||||
if obj.Arm > 0 {
|
||||
worth += obj.Arm * 100
|
||||
} else {
|
||||
worth = 10
|
||||
}
|
||||
}
|
||||
if !obj.Flags.Has(IsKnow) {
|
||||
worth /= 2
|
||||
}
|
||||
obj.Flags.Set(IsKnow)
|
||||
op.Know = true
|
||||
case Stick:
|
||||
op := &it.Sticks[obj.Which]
|
||||
worth = op.Worth
|
||||
worth += 20 * obj.Charges()
|
||||
if !obj.Flags.Has(IsKnow) {
|
||||
worth /= 2
|
||||
}
|
||||
obj.Flags.Set(IsKnow)
|
||||
op.Know = true
|
||||
case Amulet:
|
||||
worth = 1000
|
||||
}
|
||||
if worth < 0 {
|
||||
worth = 0
|
||||
}
|
||||
g.scr.Std.MvPrintw(line, 0, "%c) %5d %s", obj.PackCh, worth,
|
||||
g.invName(obj, false))
|
||||
line++
|
||||
p.Purse += worth
|
||||
}
|
||||
g.scr.Std.MvPrintw(line, 0, " %5d Gold Pieces ", oldpurse)
|
||||
g.refresh()
|
||||
g.score(p.Purse, 2, ' ')
|
||||
g.myExit(0)
|
||||
}
|
||||
|
||||
// killnameTable is the rip.c nlist[]: special death causes.
|
||||
var killnameTable = []helpEntry{
|
||||
{'a', "arrow", true},
|
||||
{'b', "bolt", true},
|
||||
{'d', "dart", true},
|
||||
{'h', "hypothermia", false},
|
||||
{'s', "starvation", false},
|
||||
}
|
||||
|
||||
// killname converts a code to a monster name (rip.c killname).
|
||||
func (g *RogueGame) killname(monst byte, doart bool) string {
|
||||
var sp string
|
||||
var article bool
|
||||
if isUpper(monst) {
|
||||
sp = g.Monsters[monst-'A'].Name
|
||||
article = true
|
||||
} else {
|
||||
sp = "Wally the Wonder Badger"
|
||||
article = false
|
||||
for _, hp := range killnameTable {
|
||||
if hp.Ch == monst {
|
||||
sp = hp.Desc
|
||||
article = hp.Print
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if doart && article {
|
||||
return "a" + vowelstr(sp) + " " + sp
|
||||
}
|
||||
return sp
|
||||
}
|
||||
|
||||
// deathMonst returns a monster appropriate for a random death (rip.c
|
||||
// death_monst).
|
||||
func (g *RogueGame) deathMonst() byte {
|
||||
poss := []byte{
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'h', 'd', 's',
|
||||
' ', // generates the "Wally the Wonder Badger" message
|
||||
}
|
||||
return poss[g.rnd(len(poss))]
|
||||
}
|
||||
196
game/score.go
Normal file
196
game/score.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// score.h / rip.c score() / save.c rd_score+wr_score — the scoreboard.
|
||||
// The C scorefile was XOR-encrypted structs; the port stores a gob list at
|
||||
// ScorePath (empty path disables persistence but still shows the list).
|
||||
|
||||
// numScores is the C numscores default: the top ten.
|
||||
const numScores = 10
|
||||
|
||||
// ScoreEnt is score.h struct sc_ent.
|
||||
type ScoreEnt struct {
|
||||
UID int
|
||||
Score int
|
||||
Flags int // 0=killed, 1=quit, 2=winner, 3=killed with amulet
|
||||
Monster byte
|
||||
Name string
|
||||
Level int
|
||||
Time int64
|
||||
}
|
||||
|
||||
var scoreReasons = [4]string{
|
||||
"killed",
|
||||
"quit",
|
||||
"A total winner",
|
||||
"killed with Amulet",
|
||||
}
|
||||
|
||||
// rdScore reads the scoreboard file (save.c rd_score).
|
||||
func (g *RogueGame) rdScore() []ScoreEnt {
|
||||
topTen := make([]ScoreEnt, numScores)
|
||||
if g.ScorePath == "" {
|
||||
return topTen
|
||||
}
|
||||
f, err := os.Open(g.ScorePath)
|
||||
if err != nil {
|
||||
return topTen
|
||||
}
|
||||
defer f.Close()
|
||||
var onDisk []ScoreEnt
|
||||
if err := gob.NewDecoder(f).Decode(&onDisk); err != nil {
|
||||
return topTen
|
||||
}
|
||||
copy(topTen, onDisk)
|
||||
return topTen
|
||||
}
|
||||
|
||||
// wrScore writes the scoreboard file (save.c wr_score).
|
||||
func (g *RogueGame) wrScore(topTen []ScoreEnt) {
|
||||
if g.ScorePath == "" {
|
||||
return
|
||||
}
|
||||
// lock_sc/unlock_sc: exclusive-create lock file with stale takeover.
|
||||
lock := g.ScorePath + ".lck"
|
||||
for range 5 {
|
||||
lf, err := os.OpenFile(lock, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
|
||||
if err == nil {
|
||||
lf.Close()
|
||||
defer os.Remove(lock)
|
||||
break
|
||||
}
|
||||
if fi, serr := os.Stat(lock); serr == nil &&
|
||||
time.Since(fi.ModTime()) > 10*time.Second {
|
||||
os.Remove(lock)
|
||||
continue
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
f, err := os.OpenFile(g.ScorePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
gob.NewEncoder(f).Encode(topTen)
|
||||
}
|
||||
|
||||
// score figures the score and posts it (rip.c score). flags -1 means just
|
||||
// display the list (the -s command line option).
|
||||
func (g *RogueGame) score(amount, flags int, monst byte) {
|
||||
if flags >= 0 && g.scr != nil && g.scr.term != nil {
|
||||
g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
|
||||
g.refresh()
|
||||
g.waitFor('\n')
|
||||
}
|
||||
|
||||
topTen := g.rdScore()
|
||||
// Insert her in list if need be
|
||||
ins := -1
|
||||
if !g.NoScore && flags >= 0 {
|
||||
uid := os.Getuid()
|
||||
scp := len(topTen)
|
||||
for i := range topTen {
|
||||
if amount > topTen[i].Score {
|
||||
scp = i
|
||||
break
|
||||
} else if !g.AllScore && flags != 2 &&
|
||||
topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
// only one score per nowin uid
|
||||
scp = len(topTen)
|
||||
break
|
||||
}
|
||||
}
|
||||
if scp < len(topTen) {
|
||||
sc2 := len(topTen) - 1
|
||||
if flags != 2 && !g.AllScore {
|
||||
for i := scp; i < len(topTen); i++ {
|
||||
if topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
sc2 = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for sc2 > scp {
|
||||
topTen[sc2] = topTen[sc2-1]
|
||||
sc2--
|
||||
}
|
||||
lvl := g.Depth
|
||||
if flags == 2 {
|
||||
lvl = g.MaxDepth
|
||||
}
|
||||
topTen[scp] = ScoreEnt{
|
||||
UID: uid,
|
||||
Score: amount,
|
||||
Flags: flags,
|
||||
Monster: monst,
|
||||
Name: g.Whoami,
|
||||
Level: lvl,
|
||||
Time: time.Now().Unix(),
|
||||
}
|
||||
ins = scp
|
||||
}
|
||||
}
|
||||
|
||||
// Build the list display
|
||||
label := "Rogueists"
|
||||
if g.AllScore {
|
||||
label = "Scores"
|
||||
}
|
||||
lines := []string{
|
||||
fmt.Sprintf("Top Ten %s:", label),
|
||||
" Score Name",
|
||||
}
|
||||
highlight := -1
|
||||
for i := range topTen {
|
||||
scp := &topTen[i]
|
||||
if scp.Score == 0 {
|
||||
break
|
||||
}
|
||||
line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1,
|
||||
scp.Score, scp.Name, scoreReasons[scp.Flags], scp.Level)
|
||||
if scp.Flags == 0 || scp.Flags == 3 {
|
||||
line += fmt.Sprintf(" by %s", g.killname(scp.Monster, true))
|
||||
}
|
||||
line += "."
|
||||
if i == ins {
|
||||
highlight = len(lines)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
if g.scr != nil && g.scr.term != nil {
|
||||
g.clear()
|
||||
for i, line := range lines {
|
||||
if i == highlight {
|
||||
g.standout()
|
||||
}
|
||||
g.mvaddstr(i, 0, line)
|
||||
if i == highlight {
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
g.refresh()
|
||||
} else {
|
||||
for _, line := range lines {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the list file
|
||||
if ins >= 0 {
|
||||
g.wrScore(topTen)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowScores implements the -s command line option: print the scoreboard
|
||||
// and nothing else.
|
||||
func (g *RogueGame) ShowScores() {
|
||||
g.NoScore = true
|
||||
g.score(0, -1, 0)
|
||||
}
|
||||
318
game/sticks.go
318
game/sticks.go
@@ -2,8 +2,322 @@ package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// sticks.c — wand and staff setup and naming. Zapping (do_zap, drain,
|
||||
// fire_bolt) arrives with the combat phase.
|
||||
// sticks.c — zap wands and staffs.
|
||||
|
||||
// doZap performs a zap with a wand (sticks.c do_zap).
|
||||
func (g *RogueGame) doZap() {
|
||||
p := &g.Player
|
||||
obj := g.getItem("zap with", int(Stick))
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if obj.Type != Stick {
|
||||
g.After = false
|
||||
g.msg("you can't zap with that!")
|
||||
return
|
||||
}
|
||||
if obj.Charges() == 0 {
|
||||
g.msg("nothing happens")
|
||||
return
|
||||
}
|
||||
switch obj.Which {
|
||||
case WsLight:
|
||||
// Reddy Kilowatt wand. Light up the room
|
||||
g.Items.Sticks[WsLight].Know = true
|
||||
if p.Room.Flags.Has(IsGone) {
|
||||
g.msg("the corridor glows and then fades")
|
||||
} else {
|
||||
p.Room.Flags.Clear(IsDark)
|
||||
// Light the room and put the player back up
|
||||
g.enterRoom(p.Pos)
|
||||
g.addmsg("the room is lit")
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" by a shimmering %s light", g.pickColor("blue"))
|
||||
}
|
||||
g.endmsg()
|
||||
}
|
||||
case WsDrain:
|
||||
// take away 1/2 of hero's hit points, then take it away evenly
|
||||
// from the monsters in the room (or next to hero if he is in a
|
||||
// passage)
|
||||
if p.Stats.HP < 2 {
|
||||
g.msg("you are too weak to use it")
|
||||
return
|
||||
}
|
||||
g.drain()
|
||||
case WsInvis, WsPolymorph, WsTelAway, WsTelTo, WsCancel:
|
||||
y := p.Pos.Y
|
||||
x := p.Pos.X
|
||||
for stepOk(g.Level.VisibleChar(y, x)) {
|
||||
y += g.Delta.Y
|
||||
x += g.Delta.X
|
||||
}
|
||||
if tp := g.Level.MonsterAt(y, x); tp != nil {
|
||||
monster := tp.Type
|
||||
if monster == 'F' {
|
||||
p.Flags.Clear(IsHeld)
|
||||
}
|
||||
switch obj.Which {
|
||||
case WsInvis:
|
||||
tp.Flags.Set(IsInvis)
|
||||
if g.cansee(y, x) {
|
||||
g.mvaddch(y, x, tp.OldCh)
|
||||
}
|
||||
case WsPolymorph:
|
||||
pp := tp.Pack
|
||||
detachMon(&g.Level.Monsters, tp)
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, g.Level.Char(y, x))
|
||||
}
|
||||
oldch := tp.OldCh
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
monster = byte(g.rnd(26) + 'A')
|
||||
g.newMonster(tp, monster, g.Delta)
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, monster)
|
||||
}
|
||||
tp.OldCh = oldch
|
||||
tp.Pack = pp
|
||||
if g.seeMonst(tp) {
|
||||
g.Items.Sticks[WsPolymorph].Know = true
|
||||
}
|
||||
case WsCancel:
|
||||
tp.Flags.Set(IsCanc)
|
||||
tp.Flags.Clear(IsInvis | CanHuh)
|
||||
tp.Disguise = tp.Type
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, tp.Disguise)
|
||||
}
|
||||
case WsTelAway, WsTelTo:
|
||||
var newPos Coord
|
||||
if obj.Which == WsTelAway {
|
||||
for {
|
||||
newPos, _ = g.findFloor(nil, 0, true)
|
||||
if newPos != p.Pos {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newPos.Y = p.Pos.Y + g.Delta.Y
|
||||
newPos.X = p.Pos.X + g.Delta.X
|
||||
}
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(IsRun)
|
||||
g.relocate(tp, newPos)
|
||||
}
|
||||
}
|
||||
case WsMissile:
|
||||
g.Items.Sticks[WsMissile].Know = true
|
||||
bolt := newObject()
|
||||
bolt.Type = '*'
|
||||
bolt.HurlDmg = "1x4"
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 1
|
||||
bolt.Flags = IsMissl
|
||||
if p.CurWeapon != nil {
|
||||
bolt.Launch = p.CurWeapon.Which
|
||||
}
|
||||
g.doMotion(bolt, g.Delta.Y, g.Delta.X)
|
||||
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
|
||||
!g.saveThrow(VsMagic, &tp.Stats) {
|
||||
g.hitMonster(bolt.Pos, bolt)
|
||||
} else if g.Options.Terse {
|
||||
g.msg("missle vanishes")
|
||||
} else {
|
||||
g.msg("the missle vanishes with a puff of smoke")
|
||||
}
|
||||
case WsHasteM, WsSlowM:
|
||||
y := p.Pos.Y
|
||||
x := p.Pos.X
|
||||
for stepOk(g.Level.VisibleChar(y, x)) {
|
||||
y += g.Delta.Y
|
||||
x += g.Delta.X
|
||||
}
|
||||
if tp := g.Level.MonsterAt(y, x); tp != nil {
|
||||
if obj.Which == WsHasteM {
|
||||
if tp.On(IsSlow) {
|
||||
tp.Flags.Clear(IsSlow)
|
||||
} else {
|
||||
tp.Flags.Set(IsHaste)
|
||||
}
|
||||
} else {
|
||||
if tp.On(IsHaste) {
|
||||
tp.Flags.Clear(IsHaste)
|
||||
} else {
|
||||
tp.Flags.Set(IsSlow)
|
||||
}
|
||||
tp.Turn = true
|
||||
}
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
g.runto(g.Delta)
|
||||
}
|
||||
case WsElect, WsFire, WsCold:
|
||||
var name string
|
||||
switch obj.Which {
|
||||
case WsElect:
|
||||
name = "bolt"
|
||||
case WsFire:
|
||||
name = "flame"
|
||||
default:
|
||||
name = "ice"
|
||||
}
|
||||
g.fireBolt(p.Pos, &g.Delta, name)
|
||||
g.Items.Sticks[obj.Which].Know = true
|
||||
case WsNop:
|
||||
}
|
||||
obj.SetCharges(obj.Charges() - 1)
|
||||
}
|
||||
|
||||
// drain does the drain-hit-points-from-player schtick (sticks.c drain).
|
||||
func (g *RogueGame) drain() {
|
||||
p := &g.Player
|
||||
// First count how many things we need to spread the hit points among
|
||||
var corp *Room
|
||||
if g.Level.Char(p.Pos.Y, p.Pos.X) == Door {
|
||||
corp = &g.Level.Passages[*g.Level.FlagsAt(p.Pos.Y, p.Pos.X)&FPNum]
|
||||
}
|
||||
inpass := p.Room.Flags.Has(IsGone)
|
||||
var drainee []*Monster
|
||||
for _, mp := range g.Level.Monsters {
|
||||
if mp.Room == p.Room || mp.Room == corp ||
|
||||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
|
||||
&g.Level.Passages[*g.Level.FlagsAt(mp.Pos.Y, mp.Pos.X)&FPNum] == p.Room) {
|
||||
drainee = append(drainee, mp)
|
||||
}
|
||||
}
|
||||
cnt := len(drainee)
|
||||
if cnt == 0 {
|
||||
g.msg("you have a tingling feeling")
|
||||
return
|
||||
}
|
||||
p.Stats.HP /= 2
|
||||
cnt = p.Stats.HP / cnt
|
||||
// Now zot all of the monsters
|
||||
for _, mp := range drainee {
|
||||
if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 {
|
||||
g.killed(mp, g.seeMonst(mp))
|
||||
} else {
|
||||
g.runto(mp.Pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fireBolt fires a bolt in a given direction from a specific starting
|
||||
// place (sticks.c fire_bolt).
|
||||
func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
p := &g.Player
|
||||
fromHero := start == p.Pos
|
||||
|
||||
bolt := newObject()
|
||||
bolt.Type = Weapon
|
||||
bolt.Which = Flame
|
||||
bolt.HurlDmg = "6x6"
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 0
|
||||
g.Items.Weapons[Flame].Name = name
|
||||
var dirch byte
|
||||
switch dir.Y + dir.X {
|
||||
case 0:
|
||||
dirch = '/'
|
||||
case 1, -1:
|
||||
if dir.Y == 0 {
|
||||
dirch = '-'
|
||||
} else {
|
||||
dirch = '|'
|
||||
}
|
||||
case 2, -2:
|
||||
dirch = '\\'
|
||||
}
|
||||
pos := start
|
||||
hitHero := !fromHero
|
||||
used := false
|
||||
changed := false
|
||||
var spotpos []Coord
|
||||
for len(spotpos) < BoltLength && !used {
|
||||
pos.Y += dir.Y
|
||||
pos.X += dir.X
|
||||
spotpos = append(spotpos, pos)
|
||||
ch := g.Level.VisibleChar(pos.Y, pos.X)
|
||||
bounce := false
|
||||
switch ch {
|
||||
case Door:
|
||||
// this code is necessary if the hero is on a door and he
|
||||
// fires at the wall the door is in, it would otherwise loop
|
||||
// infinitely
|
||||
if p.Pos != pos {
|
||||
bounce = true
|
||||
}
|
||||
case '|', '-', ' ':
|
||||
bounce = true
|
||||
}
|
||||
if bounce {
|
||||
if !changed {
|
||||
hitHero = !hitHero
|
||||
}
|
||||
changed = false
|
||||
dir.Y = -dir.Y
|
||||
dir.X = -dir.X
|
||||
spotpos = spotpos[:len(spotpos)-1]
|
||||
g.msg("the %s bounces", name)
|
||||
continue
|
||||
}
|
||||
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
|
||||
hitHero = true
|
||||
changed = !changed
|
||||
tp.OldCh = g.Level.Char(pos.Y, pos.X)
|
||||
if !g.saveThrow(VsMagic, &tp.Stats) {
|
||||
bolt.Pos = pos
|
||||
used = true
|
||||
if tp.Type == 'D' && name == "flame" {
|
||||
g.addmsg("the flame bounces")
|
||||
if !g.Options.Terse {
|
||||
g.addmsg(" off the dragon")
|
||||
}
|
||||
g.endmsg()
|
||||
} else {
|
||||
g.hitMonster(pos, bolt)
|
||||
}
|
||||
} else if ch != 'M' || tp.Disguise == 'M' {
|
||||
if fromHero {
|
||||
g.runto(pos)
|
||||
}
|
||||
if g.Options.Terse {
|
||||
g.msg("%s misses", name)
|
||||
} else {
|
||||
g.msg("the %s whizzes past %s", name, g.setMname(tp))
|
||||
}
|
||||
}
|
||||
} else if hitHero && pos == p.Pos {
|
||||
hitHero = false
|
||||
changed = !changed
|
||||
if !g.save(VsMagic) {
|
||||
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
|
||||
if fromHero {
|
||||
g.death('b')
|
||||
} else {
|
||||
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
|
||||
}
|
||||
}
|
||||
used = true
|
||||
if g.Options.Terse {
|
||||
g.msg("the %s hits", name)
|
||||
} else {
|
||||
g.msg("you are hit by the %s", name)
|
||||
}
|
||||
} else {
|
||||
g.msg("the %s whizzes by you", name)
|
||||
}
|
||||
}
|
||||
g.mvaddch(pos.Y, pos.X, dirch)
|
||||
g.refresh()
|
||||
}
|
||||
// erase the bolt trail
|
||||
for _, c2 := range spotpos {
|
||||
g.mvaddch(c2.Y, c2.X, g.Level.Char(c2.Y, c2.X))
|
||||
}
|
||||
}
|
||||
|
||||
// fixStick sets up a new wand or staff (sticks.c fix_stick).
|
||||
func (g *RogueGame) fixStick(cur *Object) {
|
||||
|
||||
25
game/term_test.go
Normal file
25
game/term_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package game
|
||||
|
||||
// testTerm is a headless Terminal for tests: rendering is a no-op and
|
||||
// input plays a script, then alternates space/newline so that --More--
|
||||
// and [Press return] prompts never block.
|
||||
type testTerm struct {
|
||||
input []byte
|
||||
pos int
|
||||
tick int
|
||||
}
|
||||
|
||||
func (t *testTerm) Render(*Window) {}
|
||||
|
||||
func (t *testTerm) ReadChar() byte {
|
||||
if t.pos < len(t.input) {
|
||||
c := t.input[t.pos]
|
||||
t.pos++
|
||||
return c
|
||||
}
|
||||
t.tick++
|
||||
if t.tick%2 == 0 {
|
||||
return '\n'
|
||||
}
|
||||
return ' '
|
||||
}
|
||||
118
game/weapons.go
118
game/weapons.go
@@ -2,11 +2,125 @@ package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// weapons.c — weapon initialization and naming. The throwing half
|
||||
// (missile, do_motion, hit_monster) arrives with the combat phase.
|
||||
// weapons.c — functions for dealing with problems brought about by weapons.
|
||||
|
||||
const noWeapon = -1
|
||||
|
||||
// missile fires a missile in a given direction (weapons.c missile).
|
||||
func (g *RogueGame) missile(ydelta, xdelta int) {
|
||||
// Get which thing we are hurling
|
||||
obj := g.getItem("throw", int(Weapon))
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
if !g.dropCheck(obj) || g.isCurrent(obj) {
|
||||
return
|
||||
}
|
||||
obj = g.leavePack(obj, true, false)
|
||||
g.doMotion(obj, ydelta, xdelta)
|
||||
// AHA! Here it has hit something. If it is a wall or a door, or if
|
||||
// it misses (combat) the monster, put it on the floor
|
||||
if g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil ||
|
||||
!g.hitMonster(obj.Pos, obj) {
|
||||
g.fall(obj, true)
|
||||
}
|
||||
}
|
||||
|
||||
// doMotion does the actual motion on the screen done by an object
|
||||
// traveling across the room (weapons.c do_motion).
|
||||
func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
|
||||
p := &g.Player
|
||||
// Come fly with us ...
|
||||
obj.Pos = p.Pos
|
||||
for {
|
||||
// Erase the old one
|
||||
if obj.Pos != p.Pos && g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
|
||||
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
|
||||
if ch == Floor && !g.showFloor() {
|
||||
ch = ' '
|
||||
}
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
// Get the new position
|
||||
obj.Pos.Y += ydelta
|
||||
obj.Pos.X += xdelta
|
||||
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
|
||||
if stepOk(ch) && ch != Door {
|
||||
// It hasn't hit anything yet, so display it if it's alright.
|
||||
if g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Type)
|
||||
g.refresh()
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// fall drops an item someplace around here (weapons.c fall).
|
||||
func (g *RogueGame) fall(obj *Object, pr bool) {
|
||||
if fpos, ok := g.fallpos(obj.Pos); ok {
|
||||
pp := g.Level.At(fpos.Y, fpos.X)
|
||||
pp.Ch = obj.Type
|
||||
obj.Pos = fpos
|
||||
if g.cansee(fpos.Y, fpos.X) {
|
||||
if pp.Monst != nil {
|
||||
pp.Monst.OldCh = obj.Type
|
||||
} else {
|
||||
g.mvaddch(fpos.Y, fpos.X, obj.Type)
|
||||
}
|
||||
}
|
||||
attachObj(&g.Level.Objects, obj)
|
||||
return
|
||||
}
|
||||
if pr {
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
}
|
||||
g.msg("the %s vanishes as it hits the ground",
|
||||
g.Items.Weapons[obj.Which].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// hitMonster checks if the missile hits the monster (weapons.c
|
||||
// hit_monster).
|
||||
func (g *RogueGame) hitMonster(mp Coord, obj *Object) bool {
|
||||
return g.fight(mp, obj, true)
|
||||
}
|
||||
|
||||
// wield pulls out a certain weapon (weapons.c wield).
|
||||
func (g *RogueGame) wield() {
|
||||
p := &g.Player
|
||||
oweapon := p.CurWeapon
|
||||
if !g.dropCheck(p.CurWeapon) {
|
||||
p.CurWeapon = oweapon
|
||||
return
|
||||
}
|
||||
p.CurWeapon = oweapon
|
||||
obj := g.getItem("wield", int(Weapon))
|
||||
if obj == nil {
|
||||
g.After = false
|
||||
return
|
||||
}
|
||||
if obj.Type == Armor {
|
||||
g.msg("you can't wield armor")
|
||||
g.After = false
|
||||
return
|
||||
}
|
||||
if g.isCurrent(obj) {
|
||||
g.After = false
|
||||
return
|
||||
}
|
||||
|
||||
sp := g.invName(obj, true)
|
||||
p.CurWeapon = obj
|
||||
if !g.Options.Terse {
|
||||
g.addmsg("you are now ")
|
||||
}
|
||||
g.msg("wielding %s (%c)", sp, obj.PackCh)
|
||||
}
|
||||
|
||||
// initWeaps is the weapons.c init_dam[] table.
|
||||
var initWeaps = [MaxWeapons]struct {
|
||||
dam string // damage when wielded
|
||||
|
||||
111
game/wizard.go
Normal file
111
game/wizard.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package game
|
||||
|
||||
// wizard.c — special wizard commands, some of which are also non-wizard
|
||||
// commands under strange circumstances. The MASTER-only debug commands
|
||||
// (create_obj, show_map) arrive with the UI phase.
|
||||
|
||||
// whatis identifies what a certain object is (wizard.c whatis).
|
||||
func (g *RogueGame) whatis(insist bool, typ int) {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you don't have anything in your pack to identify")
|
||||
return
|
||||
}
|
||||
|
||||
var obj *Object
|
||||
for {
|
||||
obj = g.getItem("identify", typ)
|
||||
if !insist {
|
||||
break
|
||||
}
|
||||
if g.NObjs == 0 {
|
||||
return
|
||||
}
|
||||
if obj == nil {
|
||||
g.msg("you must identify something")
|
||||
} else if typ != 0 && int(obj.Type) != typ &&
|
||||
!(typ == RorS && (obj.Type == Ring || obj.Type == Stick)) {
|
||||
g.msg("you must identify a %s", typeName(typ))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch obj.Type {
|
||||
case Scroll:
|
||||
setKnow(obj, g.Items.Scrolls[:])
|
||||
case Potion:
|
||||
setKnow(obj, g.Items.Potions[:])
|
||||
case Stick:
|
||||
setKnow(obj, g.Items.Sticks[:])
|
||||
case Weapon, Armor:
|
||||
obj.Flags.Set(IsKnow)
|
||||
case Ring:
|
||||
setKnow(obj, g.Items.Rings[:])
|
||||
}
|
||||
g.msg("%s", g.invName(obj, false))
|
||||
}
|
||||
|
||||
// setKnow sets things up when we really know what a thing is (wizard.c
|
||||
// set_know).
|
||||
func setKnow(obj *Object, info []ObjInfo) {
|
||||
info[obj.Which].Know = true
|
||||
obj.Flags.Set(IsKnow)
|
||||
info[obj.Which].Guess = ""
|
||||
}
|
||||
|
||||
// typeNameTable is the wizard.c type_name static tlist.
|
||||
var typeNameTable = []struct {
|
||||
ch int
|
||||
desc string
|
||||
}{
|
||||
{int(Potion), "potion"},
|
||||
{int(Scroll), "scroll"},
|
||||
{int(Food), "food"},
|
||||
{RorS, "ring, wand or staff"},
|
||||
{int(Ring), "ring"},
|
||||
{int(Stick), "wand or staff"},
|
||||
{int(Weapon), "weapon"},
|
||||
{int(Armor), "suit of armor"},
|
||||
}
|
||||
|
||||
// typeName returns the name of an object type (wizard.c type_name).
|
||||
func typeName(typ int) string {
|
||||
for _, hp := range typeNameTable {
|
||||
if typ == hp.ch {
|
||||
return hp.desc
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// teleport bamfs the hero someplace else (wizard.c teleport).
|
||||
func (g *RogueGame) teleport() {
|
||||
p := &g.Player
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
||||
c, _ := g.findFloor(nil, 0, true)
|
||||
if g.roomin(c) != p.Room {
|
||||
g.leaveRoom(p.Pos)
|
||||
p.Pos = c
|
||||
g.enterRoom(p.Pos)
|
||||
} else {
|
||||
p.Pos = c
|
||||
g.look(true)
|
||||
}
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh)
|
||||
// turn off ISHELD in case teleportation was done while fighting a
|
||||
// Flytrap
|
||||
if p.On(IsHeld) {
|
||||
p.Flags.Clear(IsHeld)
|
||||
p.VfHit = 0
|
||||
g.Monsters['F'-'A'].Stats.Dmg = "000x0"
|
||||
}
|
||||
g.NoMove = 0
|
||||
g.Count = 0
|
||||
g.Running = false
|
||||
g.flushType()
|
||||
}
|
||||
Reference in New Issue
Block a user