Refactor step 6. MessageLine (was MsgLine) owns the msg/addmsg/endmsg machinery, wired to its screen, pre---More-- redraw, and input via attach(); RogueGame keeps one-line msg/addmsgf/endmsg shorthands so the ~400 call sites are unchanged. Player gains nextPackChar and removeFromPack (the state half of pack.c leave_pack); leavePack keeps only the LastPick repeat-command tracking. Level gains ObjectAt (misc.c find_obj) and AddObject/RemoveObject/AddMonster/RemoveMonster, replacing direct attach/detach calls on the level lists. Inventory and pickup UI flows stay on RogueGame: display and orchestration, not state surgery. Behavior and RNG order unchanged; suite green.
626 lines
12 KiB
Go
626 lines
12 KiB
Go
package game
|
|
|
|
import "strconv"
|
|
|
|
// fight.c — all the fighting gets done here.
|
|
|
|
// 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(SenseMonsters) {
|
|
if g.Options.Terse {
|
|
return "it"
|
|
}
|
|
|
|
return "something"
|
|
}
|
|
|
|
var mname string
|
|
|
|
if g.Player.On(Hallucinating) {
|
|
var idx int
|
|
|
|
ch := g.mvinch(tp.Pos.Y, tp.Pos.X)
|
|
if isUpper(ch) {
|
|
idx = int(ch - 'A')
|
|
} else {
|
|
idx = g.rnd(26)
|
|
}
|
|
|
|
mname = g.Monsters[idx].Name
|
|
} else {
|
|
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(Blind) {
|
|
tp.Disguise = 'X'
|
|
if p.On(Hallucinating) {
|
|
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
|
|
}
|
|
|
|
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.rollAttacks(&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(CanConfuse) {
|
|
didHit = true
|
|
|
|
tp.Flags.Set(Confused)
|
|
p.Flags.Clear(CanConfuse)
|
|
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(Blind) {
|
|
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). removed
|
|
// reports that the monster took itself off the level during its own
|
|
// attack (the C -1 return).
|
|
func (g *RogueGame) attack(mp *Monster) (removed bool) {
|
|
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(Targeted) {
|
|
g.ToDeath = false
|
|
g.Kamikaze = false
|
|
}
|
|
|
|
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(Blind) {
|
|
mp.Disguise = 'X'
|
|
if p.On(Hallucinating) {
|
|
g.mvaddch(mp.Pos.Y, mp.Pos.X, g.randomMonsterLetter())
|
|
}
|
|
}
|
|
|
|
mname := g.setMname(mp)
|
|
oldhp := p.Stats.HP
|
|
|
|
if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) {
|
|
if mp.Type != 'I' {
|
|
if g.HasHit {
|
|
g.addmsgf(". ")
|
|
}
|
|
|
|
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(Cancelled) {
|
|
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(Awake)
|
|
|
|
if g.NoCommand == 0 {
|
|
g.addmsgf("you are frozen")
|
|
|
|
if !g.Options.Terse {
|
|
g.addmsgf(" 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(RingSustainStrength) {
|
|
g.changeStrength(-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 = g.data.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(Held)
|
|
p.VfHit++
|
|
|
|
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
|
|
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] &&
|
|
g.isMagic(obj) {
|
|
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.inventoryName(steal, true))
|
|
}
|
|
}
|
|
}
|
|
} else if mp.Type != 'I' {
|
|
if g.HasHit {
|
|
g.addmsgf(". ")
|
|
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()
|
|
|
|
return removed
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// rollAttacks rolls several attacks (fight.c roll_em).
|
|
func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
|
|
p := &g.Player
|
|
att := &thatt.Stats
|
|
def := &thdef.Stats
|
|
|
|
var (
|
|
attacks DiceSpec
|
|
hplus, dplus int
|
|
)
|
|
|
|
if weap == nil {
|
|
attacks = att.Dmg
|
|
} else {
|
|
hplus = weap.HPlus
|
|
|
|
dplus = weap.DPlus
|
|
if weap == p.CurWeapon {
|
|
if p.IsRing(Left, RingIncreaseDamage) {
|
|
dplus += p.CurRing[Left].Bonus
|
|
} else if p.IsRing(Left, RingDexterity) {
|
|
hplus += p.CurRing[Left].Bonus
|
|
}
|
|
|
|
if p.IsRing(Right, RingIncreaseDamage) {
|
|
dplus += p.CurRing[Right].Bonus
|
|
} else if p.IsRing(Right, RingDexterity) {
|
|
hplus += p.CurRing[Right].Bonus
|
|
}
|
|
}
|
|
|
|
attacks = weap.Damage
|
|
if hurl {
|
|
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
|
|
WeaponKind(p.CurWeapon.Which) == weap.Launch {
|
|
attacks = weap.HurlDmg
|
|
hplus += p.CurWeapon.HPlus
|
|
dplus += p.CurWeapon.DPlus
|
|
} else if weap.Launch < 0 {
|
|
attacks = 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(Awake) {
|
|
hplus += 4
|
|
}
|
|
|
|
defArm := def.ArmorClass
|
|
if def == &p.Stats {
|
|
if p.CurArmor != nil {
|
|
defArm = p.CurArmor.ArmorClass
|
|
}
|
|
|
|
if p.IsRing(Left, RingProtection) {
|
|
defArm -= p.CurRing[Left].Bonus
|
|
}
|
|
|
|
if p.IsRing(Right, RingProtection) {
|
|
defArm -= p.CurRing[Right].Bonus
|
|
}
|
|
}
|
|
|
|
didHit := false
|
|
|
|
for _, atk := range attacks {
|
|
if g.swing(att.Lvl, defArm, hplus+g.data.strPlus[att.Str]) {
|
|
proll := g.roll(atk.Count, atk.Sides)
|
|
|
|
damage := dplus + proll + g.data.addDam[att.Str]
|
|
if damage > 0 {
|
|
def.HP -= damage
|
|
}
|
|
|
|
didHit = true
|
|
}
|
|
}
|
|
|
|
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.Kind == KindWeapon {
|
|
g.addmsgf("the %s hits ", g.Items.Weapons[weap.Which].Name)
|
|
} else {
|
|
g.addmsgf("you hit ")
|
|
}
|
|
|
|
g.addmsgf("%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.addmsgf("%s", prname(er, true))
|
|
|
|
var s string
|
|
if g.Options.Terse {
|
|
s = " hit"
|
|
} else {
|
|
i := g.rnd(4)
|
|
if er != "" {
|
|
i += 4
|
|
}
|
|
|
|
s = g.data.hNames[i]
|
|
}
|
|
|
|
g.addmsgf("%s", s)
|
|
|
|
if !g.Options.Terse {
|
|
g.addmsgf("%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.addmsgf("%s", prname(er, true))
|
|
|
|
i := 0
|
|
if !g.Options.Terse {
|
|
i = g.rnd(4)
|
|
}
|
|
|
|
if er != "" {
|
|
i += 4
|
|
}
|
|
|
|
g.addmsgf("%s", g.data.mNames[i])
|
|
|
|
if !g.Options.Terse {
|
|
g.addmsgf(" %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.Kind == KindWeapon {
|
|
g.addmsgf("the %s misses ", g.Items.Weapons[weap.Which].Name)
|
|
} else {
|
|
g.addmsgf("you missed ")
|
|
}
|
|
|
|
g.addmsgf("%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)
|
|
g.Level.RemoveMonster(tp)
|
|
|
|
if tp.On(Targeted) {
|
|
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(Held)
|
|
p.VfHit = 0
|
|
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
|
|
case 'L':
|
|
pos, ok := g.fallpos(tp.Pos)
|
|
if ok {
|
|
tp.Room.Gold = pos
|
|
}
|
|
|
|
if ok && g.Depth >= g.MaxDepth {
|
|
gold := newObject()
|
|
gold.Kind = KindGold
|
|
|
|
gold.GoldValue = g.goldCalc()
|
|
if g.save(VsMagic) {
|
|
gold.GoldValue += 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.addmsgf(". Defeated ")
|
|
g.HasHit = false
|
|
} else {
|
|
if !g.Options.Terse {
|
|
g.addmsgf("you have ")
|
|
}
|
|
|
|
g.addmsgf("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()
|
|
}
|
|
}
|