diff --git a/game/sticks.go b/game/sticks.go index 0b2d51a..4d1d816 100644 --- a/game/sticks.go +++ b/game/sticks.go @@ -12,8 +12,6 @@ const ( // doZap performs a zap with a wand (sticks.c do_zap). func (g *RogueGame) doZap() { - p := &g.Player - obj, ok := g.promptPackItem("zap with", KindWand) if !ok { return @@ -32,182 +30,248 @@ func (g *RogueGame) doZap() { return } - switch obj.WandKind() { - case WandLight: - // Reddy Kilowatt wand. Light up the room - g.Items.Sticks[WandLight].Know = true - if p.Room.Flags.Has(Gone) { - g.msg("the corridor glows and then fades") - } else { - p.Room.Flags.Clear(Dark) - // Light the room and put the player back up - g.enterRoom(p.Pos) - g.addmsgf("the room is lit") - - if !g.Options.Terse { - g.addmsgf(" by a shimmering %s light", g.pickColor("blue")) - } - - g.endmsg() + if h := g.data.zapHandlers[obj.WandKind()]; h != nil { + if !h(g, obj) { + return // the zap aborted; no charge is used } - case WandDrainLife: - // 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 WandInvisibility, WandPolymorph, WandTeleportAway, WandTeleportTo, WandCancellation: - 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(Held) - } - - switch obj.WandKind() { - case WandInvisibility: - tp.Flags.Set(Invisible) - - if g.canSee(y, x) { - g.mvaddch(y, x, tp.OldCh) - } - case WandPolymorph: - pp := tp.Pack - g.Level.RemoveMonster(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 = g.randomMonsterLetter() - 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[WandPolymorph].Know = true - } - case WandCancellation: - tp.Flags.Set(Cancelled) - tp.Flags.Clear(Invisible | CanConfuse) - - tp.Disguise = tp.Type - if g.seeMonst(tp) { - g.mvaddch(y, x, tp.Disguise) - } - case WandTeleportAway, WandTeleportTo: - var newPos Coord - - if obj.WandKind() == WandTeleportAway { - for { - newPos, _ = g.findFloor(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(Awake) - g.relocate(tp, newPos) - } - } - case WandMagicMissile: - g.Items.Sticks[WandMagicMissile].Know = true - bolt := newObject() - bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon - bolt.HurlDmg = dice("1x4") - bolt.HPlus = 100 - bolt.DPlus = 1 - - bolt.Flags = Missile - if p.CurWeapon != nil { - bolt.Launch = WeaponKind(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") //nolint:misspell // C's spelling, kept faithfully - } else { - g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling - } - case WandHasteMonster, WandSlowMonster: - 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.WandKind() == WandHasteMonster { - if tp.On(Slowed) { - tp.Flags.Clear(Slowed) - } else { - tp.Flags.Set(Hasted) - } - } else { - if tp.On(Hasted) { - tp.Flags.Clear(Hasted) - } else { - tp.Flags.Set(Slowed) - } - - tp.Turn = true - } - - g.Delta.Y = y - g.Delta.X = x - g.runTo(g.Delta) - } - case WandLightning, WandFire, WandCold: - var name string - - switch obj.WandKind() { - case WandLightning: - name = "bolt" - case WandFire: - name = "flame" - default: - name = "ice" - } - - g.fireBolt(p.Pos, &g.Delta, name) - g.Items.Sticks[obj.Which].Know = true - case WandNothing: } obj.Charges-- } +// zapRayMonster walks the zap ray from the hero to the first blocking +// spot and returns the monster standing there, if any (the shared +// preamble of the C monster-affecting zap cases). +func (g *RogueGame) zapRayMonster() *Monster { + p := &g.Player + + y := p.Pos.Y + + x := p.Pos.X + for stepOk(g.Level.VisibleChar(y, x)) { + y += g.Delta.Y + x += g.Delta.X + } + + return g.Level.MonsterAt(y, x) +} + +// zapVictim is zapRayMonster plus the flytrap release the C code does +// before the invisibility-family effects. +func (g *RogueGame) zapVictim() *Monster { + tp := g.zapRayMonster() + if tp != nil && tp.Type == 'F' { + g.Player.Flags.Clear(Held) + } + + return tp +} + +// The per-wand effect handlers, dispatched through gameData.zapHandlers. +// Each is one case of the C do_zap switch; returning false aborts the +// zap without using a charge. + +func (g *RogueGame) zapLight(*Object) bool { + // Reddy Kilowatt wand. Light up the room + p := &g.Player + + g.Items.Sticks[WandLight].Know = true + if p.Room.Flags.Has(Gone) { + g.msg("the corridor glows and then fades") + } else { + p.Room.Flags.Clear(Dark) + // Light the room and put the player back up + g.enterRoom(p.Pos) + g.addmsgf("the room is lit") + + if !g.Options.Terse { + g.addmsgf(" by a shimmering %s light", g.pickColor("blue")) + } + + g.endmsg() + } + + return true +} + +func (g *RogueGame) zapDrainLife(*Object) bool { + // 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 g.Player.Stats.HP < 2 { + g.msg("you are too weak to use it") + + return false + } + + g.drain() + + return true +} + +func (g *RogueGame) zapInvisibility(*Object) bool { + if tp := g.zapVictim(); tp != nil { + tp.Flags.Set(Invisible) + + if g.canSee(tp.Pos.Y, tp.Pos.X) { + g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.OldCh) + } + } + + return true +} + +func (g *RogueGame) zapPolymorph(*Object) bool { + tp := g.zapVictim() + if tp == nil { + return true + } + + y, x := tp.Pos.Y, tp.Pos.X + + pp := tp.Pack + g.Level.RemoveMonster(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 := g.randomMonsterLetter() + 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[WandPolymorph].Know = true + } + + return true +} + +func (g *RogueGame) zapCancellation(*Object) bool { + if tp := g.zapVictim(); tp != nil { + tp.Flags.Set(Cancelled) + tp.Flags.Clear(Invisible | CanConfuse) + + tp.Disguise = tp.Type + if g.seeMonst(tp) { + g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Disguise) + } + } + + return true +} + +func (g *RogueGame) zapTeleport(obj *Object) bool { + p := &g.Player + + tp := g.zapVictim() + if tp == nil { + return true + } + + var newPos Coord + + if obj.WandKind() == WandTeleportAway { + for { + newPos, _ = g.findFloor(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(Awake) + g.relocate(tp, newPos) + + return true +} + +func (g *RogueGame) zapMagicMissile(*Object) bool { + p := &g.Player + + g.Items.Sticks[WandMagicMissile].Know = true + bolt := newObject() + bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon + bolt.HurlDmg = dice("1x4") + bolt.HPlus = 100 + bolt.DPlus = 1 + + bolt.Flags = Missile + if p.CurWeapon != nil { + bolt.Launch = WeaponKind(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") //nolint:misspell // C's spelling, kept faithfully + } else { + g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling + } + + return true +} + +func (g *RogueGame) zapSpeed(obj *Object) bool { + if tp := g.zapRayMonster(); tp != nil { + if obj.WandKind() == WandHasteMonster { + if tp.On(Slowed) { + tp.Flags.Clear(Slowed) + } else { + tp.Flags.Set(Hasted) + } + } else { + if tp.On(Hasted) { + tp.Flags.Clear(Hasted) + } else { + tp.Flags.Set(Slowed) + } + + tp.Turn = true + } + + g.Delta.Y = tp.Pos.Y + g.Delta.X = tp.Pos.X + g.runTo(g.Delta) + } + + return true +} + +func (g *RogueGame) zapBolt(obj *Object) bool { + var name string + + switch obj.WandKind() { + case WandLightning: + name = "bolt" + case WandFire: + name = "flame" + default: + name = "ice" + } + + g.fireBolt(g.Player.Pos, &g.Delta, name) + g.Items.Sticks[obj.Which].Know = true + + return true +} + // drain does the drain-hit-points-from-player schtick (sticks.c drain). func (g *RogueGame) drain() { p := &g.Player diff --git a/game/tables.go b/game/tables.go index 665aaed..2f9b2ac 100644 --- a/game/tables.go +++ b/game/tables.go @@ -132,6 +132,11 @@ type gameData struct { // the cases of the scrolls.c read_scroll switch. The handler gets // the scroll being read (the identify family needs its subtype). readHandlers [NumScrollTypes]func(g *RogueGame, obj *Object) + + // zapHandlers dispatches each wand kind to its effect method: the + // 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 } // ripWall is the repeated blank wall line of the tombstone art. @@ -635,6 +640,22 @@ func newGameData() *gameData { ScrollAggravateMonsters: (*RogueGame).readAggravateMonsters, ScrollProtectArmor: (*RogueGame).readProtectArmor, }, + + zapHandlers: [NumWandTypes]func(*RogueGame, *Object) bool{ + WandLight: (*RogueGame).zapLight, + WandInvisibility: (*RogueGame).zapInvisibility, + WandLightning: (*RogueGame).zapBolt, + WandFire: (*RogueGame).zapBolt, + WandCold: (*RogueGame).zapBolt, + WandPolymorph: (*RogueGame).zapPolymorph, + WandMagicMissile: (*RogueGame).zapMagicMissile, + WandHasteMonster: (*RogueGame).zapSpeed, + WandSlowMonster: (*RogueGame).zapSpeed, + WandDrainLife: (*RogueGame).zapDrainLife, + WandTeleportAway: (*RogueGame).zapTeleport, + WandTeleportTo: (*RogueGame).zapTeleport, + WandCancellation: (*RogueGame).zapCancellation, + }, } }