From a20f50065523a16284d8d00b78ecd044daa36328 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 7 Jul 2026 02:58:44 +0200 Subject: [PATCH] Decompose fight.go: hit-handler table, weapon/armor helpers (step 7) The monster special-power switch in attack becomes gameData.hitHandlers (indexed by monster letter); attack splits into monsterHit/monsterMiss; fight gains revealXeroc and heroHits; rollAttacks gains weaponAttack, wieldedRingBonus, and defenderArmor; killed gains killedSpecial. fight.go is complexity-clean. Behavior and RNG call order unchanged. --- game/fight.go | 691 ++++++++++++++++++++++++++++--------------------- game/tables.go | 16 ++ 2 files changed, 416 insertions(+), 291 deletions(-) diff --git a/game/fight.go b/game/fight.go index 53c6caf..aace02a 100644 --- a/game/fight.go +++ b/game/fight.go @@ -48,66 +48,81 @@ func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool { 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 - } + if g.revealXeroc(tp) && !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 + g.heroHits(tp, mname, weap, thrown) - 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 true } - return didHit + if thrown { + g.bounce(weap, mname, g.Options.Terse) + } else { + g.miss("", mname, g.Options.Terse) + } + + return false } -// attack has the monster attack the player (fight.c attack). removed +// 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) (removed bool) { +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. @@ -129,185 +144,12 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) { mname := g.setMname(mp) oldhp := p.Stats.HP + removed := false 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) + removed = g.monsterHit(mp, mname, oldhp) + } else { + g.monsterMiss(mp, mname) } if g.Options.FightFlush && !g.ToDeath { @@ -320,6 +162,235 @@ func (g *RogueGame) attack(mp *Monster) (removed bool) { 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) @@ -330,7 +401,6 @@ func (g *RogueGame) swing(atLvl, opArm, wplus int) bool { // 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 @@ -342,34 +412,7 @@ func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) 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 - } - } + 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. @@ -377,21 +420,7 @@ func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) 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 - } - } - + defArm := g.defenderArmor(thdef) didHit := false for _, atk := range attacks { @@ -410,6 +439,78 @@ func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) 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 { @@ -566,30 +667,7 @@ 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) - } - } + g.killedSpecial(tp) // Get rid of the monster. mname := g.setMname(tp) g.removeMon(tp.Pos, tp, true) @@ -616,6 +694,37 @@ func (g *RogueGame) killed(tp *Monster, pr bool) { } } +// 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() { diff --git a/game/tables.go b/game/tables.go index 2f9b2ac..2d66273 100644 --- a/game/tables.go +++ b/game/tables.go @@ -137,6 +137,11 @@ type gameData struct { // cases of the sticks.c do_zap switch. A false return aborts the // zap without using a charge (drain life on a too-weak hero). zapHandlers [NumWandTypes]func(g *RogueGame, obj *Object) bool + + // hitHandlers dispatches a monster's special power when its hit + // lands, indexed by monster letter - 'A': the cases of the fight.c + // attack switch. A true return means the monster removed itself. + hitHandlers [26]func(g *RogueGame, mp *Monster, mname string) bool } // ripWall is the repeated blank wall line of the tombstone art. @@ -656,6 +661,17 @@ func newGameData() *gameData { WandTeleportTo: (*RogueGame).zapTeleport, WandCancellation: (*RogueGame).zapCancellation, }, + + hitHandlers: [26]func(*RogueGame, *Monster, string) bool{ + 0: (*RogueGame).hitAquator, // 'A' - 'A' + 'F' - 'A': (*RogueGame).hitFlytrap, + 'I' - 'A': (*RogueGame).hitIceMonster, + 'L' - 'A': (*RogueGame).hitLeprechaun, + 'N' - 'A': (*RogueGame).hitNymph, + 'R' - 'A': (*RogueGame).hitRattlesnake, + 'V' - 'A': (*RogueGame).hitLifeDrainer, + 'W' - 'A': (*RogueGame).hitLifeDrainer, + }, } }