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) if g.revealXeroc(tp) && !thrown { return false } mname := g.setMname(tp) g.HasHit = g.Options.Terse && !g.ToDeath if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) { g.heroHits(tp, mname, weap, thrown) return true } if thrown { g.bounce(weap, mname, g.Options.Terse) } else { g.miss("", mname, g.Options.Terse) } return false } // revealXeroc lets him know it was really a xeroc (if it was one); it // reports whether one was unmasked (the X block of fight.c fight). func (g *RogueGame) revealXeroc(tp *Monster) bool { p := &g.Player if tp.Type != 'X' || tp.Disguise == 'X' || p.On(Blind) { return false } 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!")) return true } // heroHits lands the hero's blow on a monster: messages, the confusing // touch, and the kill check (the hit arm of fight.c fight). func (g *RogueGame) heroHits(tp *Monster, mname string, weap *Object, thrown bool) { p := &g.Player confused := false if thrown { g.thunk(weap, mname, g.Options.Terse) } else { g.hit("", mname, g.Options.Terse) } if p.On(CanConfuse) { confused = 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 confused && !p.On(Blind) { g.msg("%s appears confused", mname) } } // attack has the monster attack the player (fight.c attack). The result // reports that the monster took itself off the level during its own // attack (the C -1 return). func (g *RogueGame) attack(mp *Monster) 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 removed := false if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) { removed = g.monsterHit(mp, mname, oldhp) } else { g.monsterMiss(mp, mname) } if g.Options.FightFlush && !g.ToDeath { g.flushType() } g.Count = 0 g.status() return removed } // monsterHit lands a monster's blow on the hero: messages, death and // to-death bookkeeping, then the monster's special power (the hit arm // of fight.c attack). It reports whether the monster removed itself. func (g *RogueGame) monsterHit(mp *Monster, mname string, oldhp int) bool { p := &g.Player 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) { if h := g.data.hitHandlers[mp.Type-'A']; h != nil { return h(g, mp, mname) } } return false } // monsterMiss handles a monster's whiffed swing (the miss arm of // fight.c attack); ice monsters miss silently. func (g *RogueGame) monsterMiss(mp *Monster, mname string) { if mp.Type == 'I' { return } p := &g.Player 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) } // The monster special-power handlers, dispatched through // gameData.hitHandlers when an uncancelled monster's hit lands. Each is // one case of the C attack switch; a true return means the monster // removed itself from the level. func (g *RogueGame) hitAquator(*Monster, string) bool { // If an aquator hits, you can lose armor class. g.rustArmor(g.Player.CurArmor) return false } func (g *RogueGame) hitIceMonster(_ *Monster, mname string) bool { // The ice monster freezes you g.Player.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') } return false } func (g *RogueGame) hitRattlesnake(*Monster, string) bool { // Rattlesnakes have poisonous bites if g.save(VsPoison) { return false } if !g.Player.IsWearing(RingSustainStrength) { g.changeStrength(-1) g.msg("%s", g.chooseTerse("a bite has weakened you", "you feel a bite in your leg and now feel weaker")) } else if !g.ToDeath { g.msg("%s", g.chooseTerse("bite has no effect", "a bite momentarily weakens you")) } return false } func (g *RogueGame) hitLifeDrainer(mp *Monster, _ string) bool { // Wraiths might drain energy levels, and Vampires can steal max_hp p := &g.Player chance := 30 if mp.Type == 'W' { chance = 15 } if g.rnd(100) >= chance { return false } 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") return false } func (g *RogueGame) hitFlytrap(*Monster, string) bool { // Venus Flytrap stops the poor guy from moving p := &g.Player 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') } return false } func (g *RogueGame) hitLeprechaun(mp *Monster, _ string) bool { // Leprechaun steals some gold p := &g.Player 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) if p.Purse != lastpurse { g.msg("your purse feels lighter") } return true } func (g *RogueGame) hitNymph(mp *Monster, _ string) bool { // Nymphs steal a magic item; look through the pack and pick out one // we like. p := &g.Player 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 { return false } g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false) g.leavePack(steal, false, false) g.msg("she stole %s!", g.inventoryName(steal, true)) return true } // 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 { att := &thatt.Stats def := &thdef.Stats var ( attacks DiceSpec hplus, dplus int ) if weap == nil { attacks = att.Dmg } else { attacks, hplus, dplus = g.weaponAttack(weap, hurl) } // 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 := g.defenderArmor(thdef) 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 } // weaponAttack picks the dice and to-hit/damage bonuses a weapon swings // with: ring bonuses when wielded, and launcher pairing for hurled // missiles (the weapon preamble of fight.c roll_em). func (g *RogueGame) weaponAttack(weap *Object, hurl bool) (DiceSpec, int, int) { p := &g.Player hplus := weap.HPlus dplus := weap.DPlus if weap == p.CurWeapon { hplus, dplus = g.wieldedRingBonus(hplus, dplus) } 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 } } return attacks, hplus, dplus } // wieldedRingBonus folds damage and dexterity ring bonuses into the // wielded weapon's to-hit/damage pluses (fight.c roll_em). func (g *RogueGame) wieldedRingBonus(hplus, dplus int) (int, int) { p := &g.Player 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 } return hplus, dplus } // defenderArmor computes the defender's effective armor class: worn // armor and protection rings when the hero defends (the def_arm // computation of fight.c roll_em). func (g *RogueGame) defenderArmor(thdef *Creature) int { p := &g.Player def := &thdef.Stats 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 } } return defArm } // 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 g.killedSpecial(tp) // 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() } } // killedSpecial handles deaths with side effects: a flytrap releases its // grip and a leprechaun drops its gold (the switch of fight.c killed). func (g *RogueGame) killedSpecial(tp *Monster) { p := &g.Player // 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) } } } // 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() } }